first pass on kv store
This commit is contained in:
parent
e45d63cf24
commit
8a53fea511
1
go.mod
1
go.mod
@ -14,6 +14,7 @@ require (
|
|||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
|
2
go.sum
2
go.sum
@ -11,6 +11,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
|||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||||
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
489
modules/kv/kv.go
Normal file
489
modules/kv/kv.go
Normal file
@ -0,0 +1,489 @@
|
|||||||
|
package kv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
|
||||||
|
"github.com/goccy/go-json"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
stores = make(map[string]*Store)
|
||||||
|
mutex sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
type Store struct {
|
||||||
|
data map[string]string
|
||||||
|
expires map[string]int64
|
||||||
|
filename string
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFunctionList() map[string]luajit.GoFunction {
|
||||||
|
return map[string]luajit.GoFunction{
|
||||||
|
"kv_open": kv_open,
|
||||||
|
"kv_get": kv_get,
|
||||||
|
"kv_set": kv_set,
|
||||||
|
"kv_delete": kv_delete,
|
||||||
|
"kv_clear": kv_clear,
|
||||||
|
"kv_has": kv_has,
|
||||||
|
"kv_size": kv_size,
|
||||||
|
"kv_keys": kv_keys,
|
||||||
|
"kv_values": kv_values,
|
||||||
|
"kv_save": kv_save,
|
||||||
|
"kv_close": kv_close,
|
||||||
|
"kv_increment": kv_increment,
|
||||||
|
"kv_append": kv_append,
|
||||||
|
"kv_expire": kv_expire,
|
||||||
|
"kv_cleanup_expired": kv_cleanup_expired,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// kv_open(name, filename) -> boolean
|
||||||
|
func kv_open(s *luajit.State) int {
|
||||||
|
name := s.ToString(1)
|
||||||
|
filename := s.ToString(2)
|
||||||
|
|
||||||
|
if name == "" {
|
||||||
|
s.PushBoolean(false)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
mutex.Lock()
|
||||||
|
defer mutex.Unlock()
|
||||||
|
|
||||||
|
if _, exists := stores[name]; exists {
|
||||||
|
s.PushBoolean(true)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
store := &Store{
|
||||||
|
data: make(map[string]string),
|
||||||
|
expires: make(map[string]int64),
|
||||||
|
filename: filename,
|
||||||
|
}
|
||||||
|
|
||||||
|
if filename != "" {
|
||||||
|
store.load()
|
||||||
|
}
|
||||||
|
|
||||||
|
stores[name] = store
|
||||||
|
s.PushBoolean(true)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// kv_get(name, key, default) -> value or default
|
||||||
|
func kv_get(s *luajit.State) int {
|
||||||
|
name := s.ToString(1)
|
||||||
|
key := s.ToString(2)
|
||||||
|
hasDefault := s.GetTop() >= 3
|
||||||
|
|
||||||
|
mutex.RLock()
|
||||||
|
store, exists := stores[name]
|
||||||
|
mutex.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
if hasDefault {
|
||||||
|
s.PushCopy(3)
|
||||||
|
} else {
|
||||||
|
s.PushNil()
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
store.mutex.RLock()
|
||||||
|
value, found := store.data[key]
|
||||||
|
store.mutex.RUnlock()
|
||||||
|
|
||||||
|
if found {
|
||||||
|
s.PushString(value)
|
||||||
|
} else if hasDefault {
|
||||||
|
s.PushCopy(3)
|
||||||
|
} else {
|
||||||
|
s.PushNil()
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// kv_set(name, key, value) -> boolean
|
||||||
|
func kv_set(s *luajit.State) int {
|
||||||
|
name := s.ToString(1)
|
||||||
|
key := s.ToString(2)
|
||||||
|
value := s.ToString(3)
|
||||||
|
|
||||||
|
mutex.RLock()
|
||||||
|
store, exists := stores[name]
|
||||||
|
mutex.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
s.PushBoolean(false)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
store.mutex.Lock()
|
||||||
|
store.data[key] = value
|
||||||
|
store.mutex.Unlock()
|
||||||
|
|
||||||
|
s.PushBoolean(true)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// kv_delete(name, key) -> boolean
|
||||||
|
func kv_delete(s *luajit.State) int {
|
||||||
|
name := s.ToString(1)
|
||||||
|
key := s.ToString(2)
|
||||||
|
|
||||||
|
mutex.RLock()
|
||||||
|
store, exists := stores[name]
|
||||||
|
mutex.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
s.PushBoolean(false)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
store.mutex.Lock()
|
||||||
|
_, existed := store.data[key]
|
||||||
|
delete(store.data, key)
|
||||||
|
delete(store.expires, key)
|
||||||
|
store.mutex.Unlock()
|
||||||
|
|
||||||
|
s.PushBoolean(existed)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// kv_clear(name) -> boolean
|
||||||
|
func kv_clear(s *luajit.State) int {
|
||||||
|
name := s.ToString(1)
|
||||||
|
|
||||||
|
mutex.RLock()
|
||||||
|
store, exists := stores[name]
|
||||||
|
mutex.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
s.PushBoolean(false)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
store.mutex.Lock()
|
||||||
|
store.data = make(map[string]string)
|
||||||
|
store.expires = make(map[string]int64)
|
||||||
|
store.mutex.Unlock()
|
||||||
|
|
||||||
|
s.PushBoolean(true)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// kv_has(name, key) -> boolean
|
||||||
|
func kv_has(s *luajit.State) int {
|
||||||
|
name := s.ToString(1)
|
||||||
|
key := s.ToString(2)
|
||||||
|
|
||||||
|
mutex.RLock()
|
||||||
|
store, exists := stores[name]
|
||||||
|
mutex.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
s.PushBoolean(false)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
store.mutex.RLock()
|
||||||
|
_, found := store.data[key]
|
||||||
|
store.mutex.RUnlock()
|
||||||
|
|
||||||
|
s.PushBoolean(found)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// kv_size(name) -> number
|
||||||
|
func kv_size(s *luajit.State) int {
|
||||||
|
name := s.ToString(1)
|
||||||
|
|
||||||
|
mutex.RLock()
|
||||||
|
store, exists := stores[name]
|
||||||
|
mutex.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
s.PushNumber(0)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
store.mutex.RLock()
|
||||||
|
size := len(store.data)
|
||||||
|
store.mutex.RUnlock()
|
||||||
|
|
||||||
|
s.PushNumber(float64(size))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// kv_keys(name) -> table
|
||||||
|
func kv_keys(s *luajit.State) int {
|
||||||
|
name := s.ToString(1)
|
||||||
|
|
||||||
|
mutex.RLock()
|
||||||
|
store, exists := stores[name]
|
||||||
|
mutex.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
s.NewTable()
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
store.mutex.RLock()
|
||||||
|
keys := make([]string, 0, len(store.data))
|
||||||
|
for k := range store.data {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
store.mutex.RUnlock()
|
||||||
|
|
||||||
|
s.PushValue(keys)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// kv_values(name) -> table
|
||||||
|
func kv_values(s *luajit.State) int {
|
||||||
|
name := s.ToString(1)
|
||||||
|
|
||||||
|
mutex.RLock()
|
||||||
|
store, exists := stores[name]
|
||||||
|
mutex.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
s.NewTable()
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
store.mutex.RLock()
|
||||||
|
values := make([]string, 0, len(store.data))
|
||||||
|
for _, v := range store.data {
|
||||||
|
values = append(values, v)
|
||||||
|
}
|
||||||
|
store.mutex.RUnlock()
|
||||||
|
|
||||||
|
s.PushValue(values)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// kv_save(name) -> boolean
|
||||||
|
func kv_save(s *luajit.State) int {
|
||||||
|
name := s.ToString(1)
|
||||||
|
|
||||||
|
mutex.RLock()
|
||||||
|
store, exists := stores[name]
|
||||||
|
mutex.RUnlock()
|
||||||
|
|
||||||
|
if !exists || store.filename == "" {
|
||||||
|
s.PushBoolean(false)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
err := store.save()
|
||||||
|
s.PushBoolean(err == nil)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// kv_close(name) -> boolean
|
||||||
|
func kv_close(s *luajit.State) int {
|
||||||
|
name := s.ToString(1)
|
||||||
|
|
||||||
|
mutex.Lock()
|
||||||
|
defer mutex.Unlock()
|
||||||
|
|
||||||
|
store, exists := stores[name]
|
||||||
|
if !exists {
|
||||||
|
s.PushBoolean(false)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if store.filename != "" {
|
||||||
|
store.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(stores, name)
|
||||||
|
s.PushBoolean(true)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// kv_increment(name, key, delta) -> number
|
||||||
|
func kv_increment(s *luajit.State) int {
|
||||||
|
name := s.ToString(1)
|
||||||
|
key := s.ToString(2)
|
||||||
|
delta := 1.0
|
||||||
|
if s.GetTop() >= 3 {
|
||||||
|
delta = s.ToNumber(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutex.RLock()
|
||||||
|
store, exists := stores[name]
|
||||||
|
mutex.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
s.PushNumber(0)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
store.mutex.Lock()
|
||||||
|
current, _ := strconv.ParseFloat(store.data[key], 64)
|
||||||
|
newValue := current + delta
|
||||||
|
store.data[key] = strconv.FormatFloat(newValue, 'g', -1, 64)
|
||||||
|
store.mutex.Unlock()
|
||||||
|
|
||||||
|
s.PushNumber(newValue)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// kv_append(name, key, value, separator) -> boolean
|
||||||
|
func kv_append(s *luajit.State) int {
|
||||||
|
name := s.ToString(1)
|
||||||
|
key := s.ToString(2)
|
||||||
|
value := s.ToString(3)
|
||||||
|
separator := ""
|
||||||
|
if s.GetTop() >= 4 {
|
||||||
|
separator = s.ToString(4)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutex.RLock()
|
||||||
|
store, exists := stores[name]
|
||||||
|
mutex.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
s.PushBoolean(false)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
store.mutex.Lock()
|
||||||
|
current := store.data[key]
|
||||||
|
if current == "" {
|
||||||
|
store.data[key] = value
|
||||||
|
} else {
|
||||||
|
store.data[key] = current + separator + value
|
||||||
|
}
|
||||||
|
store.mutex.Unlock()
|
||||||
|
|
||||||
|
s.PushBoolean(true)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// kv_expire(name, key, ttl) -> boolean
|
||||||
|
func kv_expire(s *luajit.State) int {
|
||||||
|
name := s.ToString(1)
|
||||||
|
key := s.ToString(2)
|
||||||
|
ttl := s.ToNumber(3)
|
||||||
|
|
||||||
|
mutex.RLock()
|
||||||
|
store, exists := stores[name]
|
||||||
|
mutex.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
s.PushBoolean(false)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
store.mutex.Lock()
|
||||||
|
store.expires[key] = time.Now().Unix() + int64(ttl)
|
||||||
|
store.mutex.Unlock()
|
||||||
|
|
||||||
|
s.PushBoolean(true)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// kv_cleanup_expired(name) -> number
|
||||||
|
func kv_cleanup_expired(s *luajit.State) int {
|
||||||
|
name := s.ToString(1)
|
||||||
|
|
||||||
|
mutex.RLock()
|
||||||
|
store, exists := stores[name]
|
||||||
|
mutex.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
s.PushNumber(0)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTime := time.Now().Unix()
|
||||||
|
deleted := 0
|
||||||
|
|
||||||
|
store.mutex.Lock()
|
||||||
|
for key, expireTime := range store.expires {
|
||||||
|
if currentTime >= expireTime {
|
||||||
|
delete(store.data, key)
|
||||||
|
delete(store.expires, key)
|
||||||
|
deleted++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
store.mutex.Unlock()
|
||||||
|
|
||||||
|
s.PushNumber(float64(deleted))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *Store) load() error {
|
||||||
|
if store.filename == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(store.filename)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
if strings.HasSuffix(store.filename, ".json") {
|
||||||
|
decoder := json.NewDecoder(file)
|
||||||
|
return decoder.Decode(&store.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
store.data[parts[0]] = parts[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return scanner.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *Store) save() error {
|
||||||
|
if store.filename == "" {
|
||||||
|
return fmt.Errorf("no filename specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Create(store.filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
store.mutex.RLock()
|
||||||
|
defer store.mutex.RUnlock()
|
||||||
|
|
||||||
|
if strings.HasSuffix(store.filename, ".json") {
|
||||||
|
encoder := json.NewEncoder(file)
|
||||||
|
encoder.SetIndent("", "\t")
|
||||||
|
return encoder.Encode(store.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range store.data {
|
||||||
|
if _, err := fmt.Fprintf(file, "%s=%s\n", key, value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
196
modules/kv/kv.lua
Normal file
196
modules/kv/kv.lua
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
local kv = {}
|
||||||
|
|
||||||
|
-- ======================================================================
|
||||||
|
-- BASIC KEY-VALUE OPERATIONS
|
||||||
|
-- ======================================================================
|
||||||
|
|
||||||
|
function kv.open(name, filename)
|
||||||
|
if type(name) ~= "string" then error("kv.open: store name must be a string", 2) end
|
||||||
|
if filename ~= nil and type(filename) ~= "string" then error("kv.open: filename must be a string", 2) end
|
||||||
|
|
||||||
|
filename = filename or ""
|
||||||
|
return moonshark.kv_open(name, filename)
|
||||||
|
end
|
||||||
|
|
||||||
|
function kv.get(name, key, default)
|
||||||
|
if type(name) ~= "string" then error("kv.get: store name must be a string", 2) end
|
||||||
|
if type(key) ~= "string" then error("kv.get: key must be a string", 2) end
|
||||||
|
|
||||||
|
if default ~= nil then
|
||||||
|
return moonshark.kv_get(name, key, default)
|
||||||
|
else
|
||||||
|
return moonshark.kv_get(name, key)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function kv.set(name, key, value)
|
||||||
|
if type(name) ~= "string" then error("kv.set: store name must be a string", 2) end
|
||||||
|
if type(key) ~= "string" then error("kv.set: key must be a string", 2) end
|
||||||
|
if type(value) ~= "string" then error("kv.set: value must be a string", 2) end
|
||||||
|
|
||||||
|
return moonshark.kv_set(name, key, value)
|
||||||
|
end
|
||||||
|
|
||||||
|
function kv.delete(name, key)
|
||||||
|
if type(name) ~= "string" then error("kv.delete: store name must be a string", 2) end
|
||||||
|
if type(key) ~= "string" then error("kv.delete: key must be a string", 2) end
|
||||||
|
|
||||||
|
return moonshark.kv_delete(name, key)
|
||||||
|
end
|
||||||
|
|
||||||
|
function kv.clear(name)
|
||||||
|
if type(name) ~= "string" then error("kv.clear: store name must be a string", 2) end
|
||||||
|
|
||||||
|
return moonshark.kv_clear(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
function kv.has(name, key)
|
||||||
|
if type(name) ~= "string" then error("kv.has: store name must be a string", 2) end
|
||||||
|
if type(key) ~= "string" then error("kv.has: key must be a string", 2) end
|
||||||
|
|
||||||
|
return moonshark.kv_has(name, key)
|
||||||
|
end
|
||||||
|
|
||||||
|
function kv.size(name)
|
||||||
|
if type(name) ~= "string" then error("kv.size: store name must be a string", 2) end
|
||||||
|
|
||||||
|
return moonshark.kv_size(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
function kv.keys(name)
|
||||||
|
if type(name) ~= "string" then error("kv.keys: store name must be a string", 2) end
|
||||||
|
|
||||||
|
return moonshark.kv_keys(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
function kv.values(name)
|
||||||
|
if type(name) ~= "string" then error("kv.values: store name must be a string", 2) end
|
||||||
|
|
||||||
|
return moonshark.kv_values(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
function kv.save(name)
|
||||||
|
if type(name) ~= "string" then error("kv.save: store name must be a string", 2) end
|
||||||
|
|
||||||
|
return moonshark.kv_save(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
function kv.close(name)
|
||||||
|
if type(name) ~= "string" then error("kv.close: store name must be a string", 2) end
|
||||||
|
|
||||||
|
return moonshark.kv_close(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ======================================================================
|
||||||
|
-- UTILITY FUNCTIONS
|
||||||
|
-- ======================================================================
|
||||||
|
|
||||||
|
function kv.increment(name, key, delta)
|
||||||
|
if type(name) ~= "string" then error("kv.increment: store name must be a string", 2) end
|
||||||
|
if type(key) ~= "string" then error("kv.increment: key must be a string", 2) end
|
||||||
|
delta = delta or 1
|
||||||
|
if type(delta) ~= "number" then error("kv.increment: delta must be a number", 2) end
|
||||||
|
|
||||||
|
return moonshark.kv_increment(name, key, delta)
|
||||||
|
end
|
||||||
|
|
||||||
|
function kv.append(name, key, value, separator)
|
||||||
|
if type(name) ~= "string" then error("kv.append: store name must be a string", 2) end
|
||||||
|
if type(key) ~= "string" then error("kv.append: key must be a string", 2) end
|
||||||
|
if type(value) ~= "string" then error("kv.append: value must be a string", 2) end
|
||||||
|
separator = separator or ""
|
||||||
|
if type(separator) ~= "string" then error("kv.append: separator must be a string", 2) end
|
||||||
|
|
||||||
|
return moonshark.kv_append(name, key, value, separator)
|
||||||
|
end
|
||||||
|
|
||||||
|
function kv.expire(name, key, ttl)
|
||||||
|
if type(name) ~= "string" then error("kv.expire: store name must be a string", 2) end
|
||||||
|
if type(key) ~= "string" then error("kv.expire: key must be a string", 2) end
|
||||||
|
if type(ttl) ~= "number" or ttl <= 0 then error("kv.expire: TTL must be a positive number", 2) end
|
||||||
|
|
||||||
|
return moonshark.kv_expire(name, key, ttl)
|
||||||
|
end
|
||||||
|
|
||||||
|
function kv.cleanup_expired(name)
|
||||||
|
if type(name) ~= "string" then error("kv.cleanup_expired: store name must be a string", 2) end
|
||||||
|
|
||||||
|
return moonshark.kv_cleanup_expired(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ======================================================================
|
||||||
|
-- OBJECT-ORIENTED INTERFACE
|
||||||
|
-- ======================================================================
|
||||||
|
|
||||||
|
local Store = {}
|
||||||
|
Store.__index = Store
|
||||||
|
|
||||||
|
function kv.create(name, filename)
|
||||||
|
if type(name) ~= "string" then error("kv.create: store name must be a string", 2) end
|
||||||
|
if filename ~= nil and type(filename) ~= "string" then error("kv.create: filename must be a string", 2) end
|
||||||
|
|
||||||
|
local success = kv.open(name, filename)
|
||||||
|
if not success then
|
||||||
|
error("kv.create: failed to open store '" .. name .. "'", 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
return setmetatable({name = name}, Store)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Store:get(key, default)
|
||||||
|
return kv.get(self.name, key, default)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Store:set(key, value)
|
||||||
|
return kv.set(self.name, key, value)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Store:delete(key)
|
||||||
|
return kv.delete(self.name, key)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Store:clear()
|
||||||
|
return kv.clear(self.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Store:has(key)
|
||||||
|
return kv.has(self.name, key)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Store:size()
|
||||||
|
return kv.size(self.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Store:keys()
|
||||||
|
return kv.keys(self.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Store:values()
|
||||||
|
return kv.values(self.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Store:save()
|
||||||
|
return kv.save(self.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Store:close()
|
||||||
|
return kv.close(self.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Store:increment(key, delta)
|
||||||
|
return kv.increment(self.name, key, delta)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Store:append(key, value, separator)
|
||||||
|
return kv.append(self.name, key, value, separator)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Store:expire(key, ttl)
|
||||||
|
return kv.expire(self.name, key, ttl)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Store:cleanup_expired()
|
||||||
|
return kv.cleanup_expired(self.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
return kv
|
@ -8,6 +8,7 @@ import (
|
|||||||
"Moonshark/modules/crypto"
|
"Moonshark/modules/crypto"
|
||||||
"Moonshark/modules/fs"
|
"Moonshark/modules/fs"
|
||||||
"Moonshark/modules/http"
|
"Moonshark/modules/http"
|
||||||
|
"Moonshark/modules/kv"
|
||||||
"Moonshark/modules/math"
|
"Moonshark/modules/math"
|
||||||
"Moonshark/modules/sql"
|
"Moonshark/modules/sql"
|
||||||
lua_string "Moonshark/modules/string"
|
lua_string "Moonshark/modules/string"
|
||||||
@ -41,6 +42,7 @@ func New() *Registry {
|
|||||||
maps.Copy(r.goFuncs, fs.GetFunctionList())
|
maps.Copy(r.goFuncs, fs.GetFunctionList())
|
||||||
maps.Copy(r.goFuncs, http.GetFunctionList())
|
maps.Copy(r.goFuncs, http.GetFunctionList())
|
||||||
maps.Copy(r.goFuncs, sql.GetFunctionList())
|
maps.Copy(r.goFuncs, sql.GetFunctionList())
|
||||||
|
maps.Copy(r.goFuncs, kv.GetFunctionList())
|
||||||
|
|
||||||
r.loadEmbeddedModules()
|
r.loadEmbeddedModules()
|
||||||
return r
|
return r
|
||||||
|
212
modules/sessions/sessions.lua
Normal file
212
modules/sessions/sessions.lua
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
local kv = require("kv")
|
||||||
|
local crypto = require("crypto")
|
||||||
|
local json = require("json")
|
||||||
|
|
||||||
|
local sessions = {}
|
||||||
|
local stores = {}
|
||||||
|
local default_store = nil
|
||||||
|
|
||||||
|
-- ======================================================================
|
||||||
|
-- CORE FUNCTIONS
|
||||||
|
-- ======================================================================
|
||||||
|
|
||||||
|
function sessions.init(store_name, filename)
|
||||||
|
store_name = store_name or "sessions"
|
||||||
|
if not kv.open(store_name, filename) then return false end
|
||||||
|
stores[store_name] = true
|
||||||
|
if not default_store then default_store = store_name end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function sessions.create(session_id, data, store_name)
|
||||||
|
if type(session_id) ~= "string" then error("session ID must be a string", 2) end
|
||||||
|
if data ~= nil and type(data) ~= "table" then error("data must be a table", 2) end
|
||||||
|
|
||||||
|
store_name = store_name or default_store
|
||||||
|
if not store_name then error("No session store initialized", 2) end
|
||||||
|
|
||||||
|
local session_data = {
|
||||||
|
data = data or {},
|
||||||
|
_created = os.time(),
|
||||||
|
_last_accessed = os.time()
|
||||||
|
}
|
||||||
|
|
||||||
|
return kv.set(store_name, "session:" .. session_id, json.encode(session_data))
|
||||||
|
end
|
||||||
|
|
||||||
|
function sessions.get(session_id, store_name)
|
||||||
|
if type(session_id) ~= "string" then error("session ID must be a string", 2) end
|
||||||
|
|
||||||
|
store_name = store_name or default_store
|
||||||
|
if not store_name then error("No session store initialized", 2) end
|
||||||
|
|
||||||
|
local json_str = kv.get(store_name, "session:" .. session_id)
|
||||||
|
if not json_str then return nil end
|
||||||
|
|
||||||
|
local session_data = json.decode(json_str)
|
||||||
|
if not session_data then return nil end
|
||||||
|
|
||||||
|
-- Update last accessed
|
||||||
|
session_data._last_accessed = os.time()
|
||||||
|
kv.set(store_name, "session:" .. session_id, json.encode(session_data))
|
||||||
|
|
||||||
|
-- Return flattened data with metadata
|
||||||
|
local result = session_data.data or {}
|
||||||
|
result._created = session_data._created
|
||||||
|
result._last_accessed = session_data._last_accessed
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
function sessions.update(session_id, data, store_name)
|
||||||
|
if type(session_id) ~= "string" then error("session ID must be a string", 2) end
|
||||||
|
if type(data) ~= "table" then error("data must be a table", 2) end
|
||||||
|
|
||||||
|
store_name = store_name or default_store
|
||||||
|
if not store_name then error("No session store initialized", 2) end
|
||||||
|
|
||||||
|
local json_str = kv.get(store_name, "session:" .. session_id)
|
||||||
|
if not json_str then return false end
|
||||||
|
|
||||||
|
local session_data = json.decode(json_str)
|
||||||
|
if not session_data then return false end
|
||||||
|
|
||||||
|
session_data.data = data
|
||||||
|
session_data._last_accessed = os.time()
|
||||||
|
|
||||||
|
return kv.set(store_name, "session:" .. session_id, json.encode(session_data))
|
||||||
|
end
|
||||||
|
|
||||||
|
function sessions.delete(session_id, store_name)
|
||||||
|
if type(session_id) ~= "string" then error("session ID must be a string", 2) end
|
||||||
|
|
||||||
|
store_name = store_name or default_store
|
||||||
|
if not store_name then error("No session store initialized", 2) end
|
||||||
|
return kv.delete(store_name, "session:" .. session_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
function sessions.cleanup(max_age, store_name)
|
||||||
|
store_name = store_name or default_store
|
||||||
|
if not store_name then error("No session store initialized", 2) end
|
||||||
|
|
||||||
|
local keys = kv.keys(store_name)
|
||||||
|
local current_time = os.time()
|
||||||
|
local deleted = 0
|
||||||
|
|
||||||
|
for _, key in ipairs(keys) do
|
||||||
|
if key:match("^session:") then
|
||||||
|
local json_str = kv.get(store_name, key)
|
||||||
|
if json_str then
|
||||||
|
local session_data = json.decode(json_str)
|
||||||
|
if session_data and session_data._last_accessed then
|
||||||
|
if current_time - session_data._last_accessed > max_age then
|
||||||
|
kv.delete(store_name, key)
|
||||||
|
deleted = deleted + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return deleted
|
||||||
|
end
|
||||||
|
|
||||||
|
function sessions.close(store_name)
|
||||||
|
local success = kv.close(store_name)
|
||||||
|
stores[store_name] = nil
|
||||||
|
if default_store == store_name then
|
||||||
|
default_store = next(stores)
|
||||||
|
end
|
||||||
|
return success
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ======================================================================
|
||||||
|
-- UTILITIES
|
||||||
|
-- ======================================================================
|
||||||
|
|
||||||
|
function sessions.generate_id()
|
||||||
|
return crypto.random_alphanumeric(32)
|
||||||
|
end
|
||||||
|
|
||||||
|
function sessions.exists(session_id, store_name)
|
||||||
|
store_name = store_name or default_store
|
||||||
|
if not store_name then error("No session store initialized", 2) end
|
||||||
|
return kv.has(store_name, "session:" .. session_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
function sessions.list(store_name)
|
||||||
|
store_name = store_name or default_store
|
||||||
|
if not store_name then error("No session store initialized", 2) end
|
||||||
|
|
||||||
|
local keys = kv.keys(store_name)
|
||||||
|
local session_ids = {}
|
||||||
|
|
||||||
|
for _, key in ipairs(keys) do
|
||||||
|
local session_id = key:match("^session:(.+)")
|
||||||
|
if session_id then
|
||||||
|
table.insert(session_ids, session_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return session_ids
|
||||||
|
end
|
||||||
|
|
||||||
|
function sessions.count(store_name)
|
||||||
|
return #sessions.list(store_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
function sessions.reset()
|
||||||
|
stores = {}
|
||||||
|
default_store = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ======================================================================
|
||||||
|
-- OOP INTERFACE
|
||||||
|
-- ======================================================================
|
||||||
|
|
||||||
|
local SessionStore = {}
|
||||||
|
SessionStore.__index = SessionStore
|
||||||
|
|
||||||
|
function sessions.create_store(store_name, filename)
|
||||||
|
if not sessions.init(store_name, filename) then
|
||||||
|
error("Failed to initialize store '" .. store_name .. "'", 2)
|
||||||
|
end
|
||||||
|
return setmetatable({name = store_name}, SessionStore)
|
||||||
|
end
|
||||||
|
|
||||||
|
function SessionStore:create(session_id, data)
|
||||||
|
return sessions.create(session_id, data, self.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
function SessionStore:get(session_id)
|
||||||
|
return sessions.get(session_id, self.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
function SessionStore:update(session_id, data)
|
||||||
|
return sessions.update(session_id, data, self.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
function SessionStore:delete(session_id)
|
||||||
|
return sessions.delete(session_id, self.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
function SessionStore:cleanup(max_age)
|
||||||
|
return sessions.cleanup(max_age, self.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
function SessionStore:exists(session_id)
|
||||||
|
return sessions.exists(session_id, self.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
function SessionStore:list()
|
||||||
|
return sessions.list(self.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
function SessionStore:count()
|
||||||
|
return sessions.count(self.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
function SessionStore:close()
|
||||||
|
return sessions.close(self.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
return sessions
|
358
tests/kv.lua
Normal file
358
tests/kv.lua
Normal file
@ -0,0 +1,358 @@
|
|||||||
|
require("tests")
|
||||||
|
local kv = require("kv")
|
||||||
|
|
||||||
|
-- Clean up any existing test files
|
||||||
|
os.remove("test_store.json")
|
||||||
|
os.remove("test_store.txt")
|
||||||
|
os.remove("test_oop.json")
|
||||||
|
os.remove("test_temp.json")
|
||||||
|
|
||||||
|
-- ======================================================================
|
||||||
|
-- BASIC OPERATIONS
|
||||||
|
-- ======================================================================
|
||||||
|
|
||||||
|
test("Store creation and opening", function()
|
||||||
|
assert(kv.open("test", "test_store.json"))
|
||||||
|
assert(kv.open("memory_only", ""))
|
||||||
|
assert(not kv.open("", "test.json")) -- Empty name should fail
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("Set and get operations", function()
|
||||||
|
assert(kv.set("test", "key1", "value1"))
|
||||||
|
assert_equal("value1", kv.get("test", "key1"))
|
||||||
|
assert_equal("default", kv.get("test", "nonexistent", "default"))
|
||||||
|
assert_equal(nil, kv.get("test", "nonexistent"))
|
||||||
|
|
||||||
|
-- Test with special characters
|
||||||
|
assert(kv.set("test", "special:key", "value with spaces & symbols!"))
|
||||||
|
assert_equal("value with spaces & symbols!", kv.get("test", "special:key"))
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("Key existence and deletion", function()
|
||||||
|
kv.set("test", "temp_key", "temp_value")
|
||||||
|
assert(kv.has("test", "temp_key"))
|
||||||
|
assert(not kv.has("test", "missing_key"))
|
||||||
|
|
||||||
|
assert(kv.delete("test", "temp_key"))
|
||||||
|
assert(not kv.has("test", "temp_key"))
|
||||||
|
assert(not kv.delete("test", "missing_key"))
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("Store size tracking", function()
|
||||||
|
kv.clear("test")
|
||||||
|
assert_equal(0, kv.size("test"))
|
||||||
|
|
||||||
|
kv.set("test", "k1", "v1")
|
||||||
|
kv.set("test", "k2", "v2")
|
||||||
|
kv.set("test", "k3", "v3")
|
||||||
|
assert_equal(3, kv.size("test"))
|
||||||
|
|
||||||
|
kv.delete("test", "k2")
|
||||||
|
assert_equal(2, kv.size("test"))
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("Keys and values retrieval", function()
|
||||||
|
kv.clear("test")
|
||||||
|
kv.set("test", "a", "apple")
|
||||||
|
kv.set("test", "b", "banana")
|
||||||
|
kv.set("test", "c", "cherry")
|
||||||
|
|
||||||
|
local keys = kv.keys("test")
|
||||||
|
local values = kv.values("test")
|
||||||
|
|
||||||
|
assert_equal(3, #keys)
|
||||||
|
assert_equal(3, #values)
|
||||||
|
|
||||||
|
-- Check all keys exist
|
||||||
|
local key_set = {}
|
||||||
|
for _, k in ipairs(keys) do
|
||||||
|
key_set[k] = true
|
||||||
|
end
|
||||||
|
assert(key_set["a"] and key_set["b"] and key_set["c"])
|
||||||
|
|
||||||
|
-- Check all values exist
|
||||||
|
local value_set = {}
|
||||||
|
for _, v in ipairs(values) do
|
||||||
|
value_set[v] = true
|
||||||
|
end
|
||||||
|
assert(value_set["apple"] and value_set["banana"] and value_set["cherry"])
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("Clear store", function()
|
||||||
|
kv.set("test", "temp1", "value1")
|
||||||
|
kv.set("test", "temp2", "value2")
|
||||||
|
assert(kv.size("test") > 0)
|
||||||
|
|
||||||
|
assert(kv.clear("test"))
|
||||||
|
assert_equal(0, kv.size("test"))
|
||||||
|
assert(not kv.has("test", "temp1"))
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("Save and close operations", function()
|
||||||
|
kv.set("test", "persistent", "data")
|
||||||
|
assert(kv.save("test"))
|
||||||
|
assert(kv.close("test"))
|
||||||
|
|
||||||
|
-- Reopen and verify data persists
|
||||||
|
assert(kv.open("test", "test_store.json"))
|
||||||
|
assert_equal("data", kv.get("test", "persistent"))
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("Invalid store operations", function()
|
||||||
|
assert(not kv.set("nonexistent", "key", "value"))
|
||||||
|
assert_equal(nil, kv.get("nonexistent", "key"))
|
||||||
|
assert(not kv.has("nonexistent", "key"))
|
||||||
|
assert_equal(0, kv.size("nonexistent"))
|
||||||
|
assert(not kv.delete("nonexistent", "key"))
|
||||||
|
assert(not kv.clear("nonexistent"))
|
||||||
|
assert(not kv.save("nonexistent"))
|
||||||
|
assert(not kv.close("nonexistent"))
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- ======================================================================
|
||||||
|
-- OBJECT-ORIENTED INTERFACE
|
||||||
|
-- ======================================================================
|
||||||
|
|
||||||
|
test("OOP store creation", function()
|
||||||
|
local store = kv.create("oop_test", "test_oop.json")
|
||||||
|
assert_equal("oop_test", store.name)
|
||||||
|
|
||||||
|
local memory_store = kv.create("memory_oop")
|
||||||
|
assert_equal("memory_oop", memory_store.name)
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("OOP basic operations", function()
|
||||||
|
local store = kv.create("oop_basic")
|
||||||
|
|
||||||
|
assert(store:set("foo", "bar"))
|
||||||
|
assert_equal("bar", store:get("foo"))
|
||||||
|
assert_equal("default", store:get("missing", "default"))
|
||||||
|
assert(store:has("foo"))
|
||||||
|
assert(not store:has("missing"))
|
||||||
|
assert_equal(1, store:size())
|
||||||
|
|
||||||
|
assert(store:delete("foo"))
|
||||||
|
assert(not store:has("foo"))
|
||||||
|
assert_equal(0, store:size())
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("OOP collections", function()
|
||||||
|
local store = kv.create("oop_collections")
|
||||||
|
|
||||||
|
store:set("a", "apple")
|
||||||
|
store:set("b", "banana")
|
||||||
|
store:set("c", "cherry")
|
||||||
|
|
||||||
|
local keys = store:keys()
|
||||||
|
local values = store:values()
|
||||||
|
|
||||||
|
assert_equal(3, #keys)
|
||||||
|
assert_equal(3, #values)
|
||||||
|
|
||||||
|
store:clear()
|
||||||
|
assert_equal(0, store:size())
|
||||||
|
|
||||||
|
store:close()
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- ======================================================================
|
||||||
|
-- UTILITY FUNCTIONS
|
||||||
|
-- ======================================================================
|
||||||
|
|
||||||
|
test("Increment operations", function()
|
||||||
|
kv.open("util_test")
|
||||||
|
|
||||||
|
-- Increment non-existent key
|
||||||
|
assert_equal(1, kv.increment("util_test", "counter"))
|
||||||
|
assert_equal("1", kv.get("util_test", "counter"))
|
||||||
|
|
||||||
|
-- Increment existing key
|
||||||
|
assert_equal(6, kv.increment("util_test", "counter", 5))
|
||||||
|
assert_equal("6", kv.get("util_test", "counter"))
|
||||||
|
|
||||||
|
-- Decrement
|
||||||
|
assert_equal(4, kv.increment("util_test", "counter", -2))
|
||||||
|
|
||||||
|
kv.close("util_test")
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("Append operations", function()
|
||||||
|
kv.open("append_test")
|
||||||
|
|
||||||
|
-- Append to non-existent key
|
||||||
|
assert(kv.append("append_test", "list", "first"))
|
||||||
|
assert_equal("first", kv.get("append_test", "list"))
|
||||||
|
|
||||||
|
-- Append with separator
|
||||||
|
assert(kv.append("append_test", "list", "second", ","))
|
||||||
|
assert_equal("first,second", kv.get("append_test", "list"))
|
||||||
|
|
||||||
|
assert(kv.append("append_test", "list", "third", ","))
|
||||||
|
assert_equal("first,second,third", kv.get("append_test", "list"))
|
||||||
|
|
||||||
|
kv.close("append_test")
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("TTL and expiration", function()
|
||||||
|
kv.open("ttl_test", "test_temp.json")
|
||||||
|
|
||||||
|
kv.set("ttl_test", "temp_key", "temp_value")
|
||||||
|
assert(kv.expire("ttl_test", "temp_key", 1)) -- 1 second TTL
|
||||||
|
|
||||||
|
-- Key should still exist immediately
|
||||||
|
assert(kv.has("ttl_test", "temp_key"))
|
||||||
|
|
||||||
|
-- Wait for expiration
|
||||||
|
os.execute("sleep 2")
|
||||||
|
local expired = kv.cleanup_expired("ttl_test")
|
||||||
|
assert(expired >= 1)
|
||||||
|
|
||||||
|
-- Key should be gone
|
||||||
|
assert(not kv.has("ttl_test", "temp_key"))
|
||||||
|
|
||||||
|
kv.close("ttl_test")
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- ======================================================================
|
||||||
|
-- FILE FORMAT TESTS
|
||||||
|
-- ======================================================================
|
||||||
|
|
||||||
|
test("JSON file format", function()
|
||||||
|
kv.open("json_test", "test.json")
|
||||||
|
kv.set("json_test", "key1", "value1")
|
||||||
|
kv.set("json_test", "key2", "value2")
|
||||||
|
kv.save("json_test")
|
||||||
|
kv.close("json_test")
|
||||||
|
|
||||||
|
-- Verify file exists and reload
|
||||||
|
assert(file_exists("test.json"))
|
||||||
|
kv.open("json_test", "test.json")
|
||||||
|
assert_equal("value1", kv.get("json_test", "key1"))
|
||||||
|
assert_equal("value2", kv.get("json_test", "key2"))
|
||||||
|
kv.close("json_test")
|
||||||
|
|
||||||
|
os.remove("test.json")
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("Text file format", function()
|
||||||
|
kv.open("txt_test", "test.txt")
|
||||||
|
kv.set("txt_test", "setting1", "value1")
|
||||||
|
kv.set("txt_test", "setting2", "value2")
|
||||||
|
kv.save("txt_test")
|
||||||
|
kv.close("txt_test")
|
||||||
|
|
||||||
|
-- Verify file exists and reload
|
||||||
|
assert(file_exists("test.txt"))
|
||||||
|
kv.open("txt_test", "test.txt")
|
||||||
|
assert_equal("value1", kv.get("txt_test", "setting1"))
|
||||||
|
assert_equal("value2", kv.get("txt_test", "setting2"))
|
||||||
|
kv.close("txt_test")
|
||||||
|
|
||||||
|
os.remove("test.txt")
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- ======================================================================
|
||||||
|
-- EDGE CASES AND ERROR HANDLING
|
||||||
|
-- ======================================================================
|
||||||
|
|
||||||
|
test("Empty values and keys", function()
|
||||||
|
kv.open("edge_test")
|
||||||
|
|
||||||
|
-- Empty value
|
||||||
|
assert(kv.set("edge_test", "empty", ""))
|
||||||
|
assert_equal("", kv.get("edge_test", "empty"))
|
||||||
|
|
||||||
|
-- Unicode keys and values
|
||||||
|
assert(kv.set("edge_test", "ключ", "значение"))
|
||||||
|
assert_equal("значение", kv.get("edge_test", "ключ"))
|
||||||
|
|
||||||
|
kv.close("edge_test")
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("Special characters in data", function()
|
||||||
|
kv.open("special_test")
|
||||||
|
|
||||||
|
local special_value = 'Special chars: "quotes", \'apostrophes\', \n newlines, \t tabs, \\ backslashes'
|
||||||
|
assert(kv.set("special_test", "special", special_value))
|
||||||
|
assert_equal(special_value, kv.get("special_test", "special"))
|
||||||
|
|
||||||
|
kv.close("special_test")
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("Large data handling", function()
|
||||||
|
kv.open("large_test")
|
||||||
|
|
||||||
|
-- Large value
|
||||||
|
local large_value = string.rep("x", 10000)
|
||||||
|
assert(kv.set("large_test", "large", large_value))
|
||||||
|
assert_equal(large_value, kv.get("large_test", "large"))
|
||||||
|
|
||||||
|
-- Many keys
|
||||||
|
for i = 1, 100 do
|
||||||
|
kv.set("large_test", "key" .. i, "value" .. i)
|
||||||
|
end
|
||||||
|
assert_equal(101, kv.size("large_test")) -- 100 + 1 large value
|
||||||
|
|
||||||
|
kv.close("large_test")
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- ======================================================================
|
||||||
|
-- PERFORMANCE TESTS
|
||||||
|
-- ======================================================================
|
||||||
|
|
||||||
|
test("Performance test", function()
|
||||||
|
kv.open("perf_test")
|
||||||
|
|
||||||
|
local start = os.clock()
|
||||||
|
|
||||||
|
-- Bulk insert
|
||||||
|
for i = 1, 1000 do
|
||||||
|
kv.set("perf_test", "key" .. i, "value" .. i)
|
||||||
|
end
|
||||||
|
local insert_time = os.clock() - start
|
||||||
|
|
||||||
|
-- Bulk read
|
||||||
|
start = os.clock()
|
||||||
|
for i = 1, 1000 do
|
||||||
|
local value = kv.get("perf_test", "key" .. i)
|
||||||
|
assert_equal("value" .. i, value)
|
||||||
|
end
|
||||||
|
local read_time = os.clock() - start
|
||||||
|
|
||||||
|
-- Check final size
|
||||||
|
assert_equal(1000, kv.size("perf_test"))
|
||||||
|
|
||||||
|
print(string.format(" Insert 1000 items: %.3fs", insert_time))
|
||||||
|
print(string.format(" Read 1000 items: %.3fs", read_time))
|
||||||
|
|
||||||
|
kv.close("perf_test")
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- ======================================================================
|
||||||
|
-- INTEGRATION TESTS
|
||||||
|
-- ======================================================================
|
||||||
|
|
||||||
|
test("Multiple store integration", function()
|
||||||
|
local users = kv.create("users_int")
|
||||||
|
local cache = kv.create("cache_int")
|
||||||
|
|
||||||
|
-- Simulate user data
|
||||||
|
users:set("user:123", "john_doe")
|
||||||
|
cache:set("user:123:last_seen", tostring(os.time()))
|
||||||
|
|
||||||
|
-- Verify data in stores
|
||||||
|
assert_equal("john_doe", users:get("user:123"))
|
||||||
|
assert(cache:has("user:123:last_seen"))
|
||||||
|
|
||||||
|
-- Clean up
|
||||||
|
users:close()
|
||||||
|
cache:close()
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Clean up test files
|
||||||
|
--os.remove("test_store.json")
|
||||||
|
--os.remove("test_oop.json")
|
||||||
|
--os.remove("test_temp.json")
|
||||||
|
|
||||||
|
summary()
|
||||||
|
test_exit()
|
397
tests/sessions.lua
Normal file
397
tests/sessions.lua
Normal file
@ -0,0 +1,397 @@
|
|||||||
|
require("tests")
|
||||||
|
local sessions = require("sessions")
|
||||||
|
|
||||||
|
-- Clean up test files
|
||||||
|
os.remove("test_sessions.json")
|
||||||
|
os.remove("test_sessions2.json")
|
||||||
|
|
||||||
|
-- ======================================================================
|
||||||
|
-- SESSION STORE INITIALIZATION
|
||||||
|
-- ======================================================================
|
||||||
|
|
||||||
|
test("Session store initialization", function()
|
||||||
|
assert(sessions.init("test_sessions", "test_sessions.json"))
|
||||||
|
assert(sessions.init("memory_sessions"))
|
||||||
|
|
||||||
|
-- Test with explicit store name
|
||||||
|
assert(sessions.init("named_sessions", "test_sessions2.json"))
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("Session ID generation", function()
|
||||||
|
local id1 = sessions.generate_id()
|
||||||
|
local id2 = sessions.generate_id()
|
||||||
|
|
||||||
|
assert_equal(32, #id1)
|
||||||
|
assert_equal(32, #id2)
|
||||||
|
assert(id1 ~= id2, "Session IDs should be unique")
|
||||||
|
|
||||||
|
-- Check character set (alphanumeric)
|
||||||
|
assert(id1:match("^[a-zA-Z0-9]+$"), "Session ID should be alphanumeric")
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- ======================================================================
|
||||||
|
-- BASIC SESSION OPERATIONS
|
||||||
|
-- ======================================================================
|
||||||
|
|
||||||
|
test("Session creation and retrieval", function()
|
||||||
|
sessions.init("basic_test")
|
||||||
|
|
||||||
|
local session_id = sessions.generate_id()
|
||||||
|
local session_data = {
|
||||||
|
user_id = 123,
|
||||||
|
username = "testuser",
|
||||||
|
role = "admin",
|
||||||
|
permissions = {"read", "write"}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(sessions.create(session_id, session_data))
|
||||||
|
|
||||||
|
local retrieved = sessions.get(session_id)
|
||||||
|
assert_equal(123, retrieved.user_id)
|
||||||
|
assert_equal("testuser", retrieved.username)
|
||||||
|
assert_equal("admin", retrieved.role)
|
||||||
|
assert_table_equal({"read", "write"}, retrieved.permissions)
|
||||||
|
assert(retrieved._created ~= nil)
|
||||||
|
assert(retrieved._last_accessed ~= nil)
|
||||||
|
|
||||||
|
-- Test non-existent session
|
||||||
|
assert_equal(nil, sessions.get("nonexistent"))
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("Session updates", function()
|
||||||
|
sessions.init("update_test")
|
||||||
|
|
||||||
|
local session_id = sessions.generate_id()
|
||||||
|
sessions.create(session_id, {count = 1})
|
||||||
|
|
||||||
|
local session = sessions.get(session_id)
|
||||||
|
local new_data = {
|
||||||
|
count = 2,
|
||||||
|
new_field = "added"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(sessions.update(session_id, new_data))
|
||||||
|
|
||||||
|
local updated = sessions.get(session_id)
|
||||||
|
assert_equal(2, updated.count)
|
||||||
|
assert_equal("added", updated.new_field)
|
||||||
|
assert(updated._created ~= nil)
|
||||||
|
assert(updated._last_accessed ~= nil)
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("Session deletion", function()
|
||||||
|
sessions.init("delete_test")
|
||||||
|
|
||||||
|
local session_id = sessions.generate_id()
|
||||||
|
sessions.create(session_id, {temp = true})
|
||||||
|
|
||||||
|
assert(sessions.get(session_id) ~= nil)
|
||||||
|
assert(sessions.delete(session_id))
|
||||||
|
assert_equal(nil, sessions.get(session_id))
|
||||||
|
|
||||||
|
-- Delete non-existent session
|
||||||
|
assert(not sessions.delete("nonexistent"))
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("Session existence check", function()
|
||||||
|
sessions.init("exists_test")
|
||||||
|
|
||||||
|
local session_id = sessions.generate_id()
|
||||||
|
assert(not sessions.exists(session_id))
|
||||||
|
|
||||||
|
sessions.create(session_id, {test = true})
|
||||||
|
assert(sessions.exists(session_id))
|
||||||
|
|
||||||
|
sessions.delete(session_id)
|
||||||
|
assert(not sessions.exists(session_id))
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- ======================================================================
|
||||||
|
-- MULTI-STORE OPERATIONS
|
||||||
|
-- ======================================================================
|
||||||
|
|
||||||
|
test("Multiple session stores", function()
|
||||||
|
assert(sessions.init("store1", "store1.json"))
|
||||||
|
assert(sessions.init("store2", "store2.json"))
|
||||||
|
|
||||||
|
local id1 = sessions.generate_id()
|
||||||
|
local id2 = sessions.generate_id()
|
||||||
|
|
||||||
|
-- Create sessions in different stores
|
||||||
|
assert(sessions.create(id1, {store = "store1"}, "store1"))
|
||||||
|
assert(sessions.create(id2, {store = "store2"}, "store2"))
|
||||||
|
|
||||||
|
-- Verify isolation
|
||||||
|
assert_equal(nil, sessions.get(id1, "store2"))
|
||||||
|
assert_equal(nil, sessions.get(id2, "store1"))
|
||||||
|
|
||||||
|
-- Verify correct retrieval
|
||||||
|
local s1 = sessions.get(id1, "store1")
|
||||||
|
local s2 = sessions.get(id2, "store2")
|
||||||
|
assert_equal("store1", s1.store)
|
||||||
|
assert_equal("store2", s2.store)
|
||||||
|
|
||||||
|
os.remove("store1.json")
|
||||||
|
os.remove("store2.json")
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("Default store behavior", function()
|
||||||
|
sessions.reset()
|
||||||
|
sessions.init("default_store")
|
||||||
|
|
||||||
|
local session_id = sessions.generate_id()
|
||||||
|
sessions.create(session_id, {test = "default"})
|
||||||
|
|
||||||
|
-- Should work without specifying store
|
||||||
|
local retrieved = sessions.get(session_id)
|
||||||
|
assert_equal("default", retrieved.test)
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- ======================================================================
|
||||||
|
-- SESSION CLEANUP
|
||||||
|
-- ======================================================================
|
||||||
|
|
||||||
|
test("Session cleanup", function()
|
||||||
|
sessions.init("cleanup_test")
|
||||||
|
|
||||||
|
local old_session = sessions.generate_id()
|
||||||
|
local new_session = sessions.generate_id()
|
||||||
|
|
||||||
|
sessions.create(old_session, {test = "old"}, "cleanup_test")
|
||||||
|
sessions.create(new_session, {test = "new"}, "cleanup_test")
|
||||||
|
|
||||||
|
-- Wait to create age difference
|
||||||
|
os.execute("sleep 2")
|
||||||
|
|
||||||
|
-- Access new session to update timestamp
|
||||||
|
sessions.get(new_session, "cleanup_test")
|
||||||
|
|
||||||
|
-- Clean up sessions older than 1 second
|
||||||
|
local deleted = sessions.cleanup(1, "cleanup_test")
|
||||||
|
assert(deleted >= 1)
|
||||||
|
|
||||||
|
-- New session should remain (recently accessed)
|
||||||
|
assert(sessions.get(new_session, "cleanup_test") ~= nil)
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- ======================================================================
|
||||||
|
-- SESSION LISTING AND COUNTING
|
||||||
|
-- ======================================================================
|
||||||
|
|
||||||
|
test("Session listing and counting", function()
|
||||||
|
sessions.init("list_test")
|
||||||
|
|
||||||
|
local ids = {}
|
||||||
|
for i = 1, 5 do
|
||||||
|
local id = sessions.generate_id()
|
||||||
|
sessions.create(id, {index = i}, "list_test")
|
||||||
|
table.insert(ids, id)
|
||||||
|
end
|
||||||
|
|
||||||
|
local session_list = sessions.list("list_test")
|
||||||
|
assert_equal(5, #session_list)
|
||||||
|
|
||||||
|
local count = sessions.count("list_test")
|
||||||
|
assert_equal(5, count)
|
||||||
|
|
||||||
|
-- Verify all IDs are in the list
|
||||||
|
local id_set = {}
|
||||||
|
for _, id in ipairs(session_list) do
|
||||||
|
id_set[id] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, id in ipairs(ids) do
|
||||||
|
assert(id_set[id], "Session ID should be in list")
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- ======================================================================
|
||||||
|
-- OBJECT-ORIENTED INTERFACE
|
||||||
|
-- ======================================================================
|
||||||
|
|
||||||
|
test("OOP session store", function()
|
||||||
|
local store = sessions.create_store("oop_sessions", "oop_test.json")
|
||||||
|
|
||||||
|
local session_id = sessions.generate_id()
|
||||||
|
local data = {user = "oop_test", role = "admin"}
|
||||||
|
|
||||||
|
assert(store:create(session_id, data))
|
||||||
|
|
||||||
|
local retrieved = store:get(session_id)
|
||||||
|
assert_equal("oop_test", retrieved.user)
|
||||||
|
assert_equal("admin", retrieved.role)
|
||||||
|
|
||||||
|
retrieved.last_action = "login"
|
||||||
|
assert(store:update(session_id, retrieved))
|
||||||
|
|
||||||
|
local updated = store:get(session_id)
|
||||||
|
assert_equal("login", updated.last_action)
|
||||||
|
|
||||||
|
assert(store:exists(session_id))
|
||||||
|
assert_equal(1, store:count())
|
||||||
|
|
||||||
|
local session_list = store:list()
|
||||||
|
assert_equal(1, #session_list)
|
||||||
|
assert_equal(session_id, session_list[1])
|
||||||
|
|
||||||
|
assert(store:delete(session_id))
|
||||||
|
assert(not store:exists(session_id))
|
||||||
|
|
||||||
|
store:close()
|
||||||
|
os.remove("oop_test.json")
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- ======================================================================
|
||||||
|
-- ERROR HANDLING
|
||||||
|
-- ======================================================================
|
||||||
|
|
||||||
|
test("Session error handling", function()
|
||||||
|
sessions.reset()
|
||||||
|
|
||||||
|
-- Try operations without initialization
|
||||||
|
local success1, err1 = pcall(sessions.create, "test_id", {})
|
||||||
|
assert(not success1)
|
||||||
|
|
||||||
|
local success2, err2 = pcall(sessions.get, "test_id")
|
||||||
|
assert(not success2)
|
||||||
|
|
||||||
|
-- Initialize and test invalid inputs
|
||||||
|
sessions.init("error_test")
|
||||||
|
|
||||||
|
-- Invalid session ID type
|
||||||
|
local success3, err3 = pcall(sessions.create, 123, {})
|
||||||
|
assert(not success3)
|
||||||
|
|
||||||
|
-- Invalid data type
|
||||||
|
local success4, err4 = pcall(sessions.create, "test", "not_a_table")
|
||||||
|
assert(not success4)
|
||||||
|
|
||||||
|
-- Invalid store name
|
||||||
|
local success5, err5 = pcall(sessions.get, "test", 123)
|
||||||
|
assert(not success5)
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- ======================================================================
|
||||||
|
-- DATA PERSISTENCE
|
||||||
|
-- ======================================================================
|
||||||
|
|
||||||
|
test("Session persistence", function()
|
||||||
|
sessions.init("persist_test", "persist_sessions.json")
|
||||||
|
|
||||||
|
local session_id = sessions.generate_id()
|
||||||
|
local data = {
|
||||||
|
user_id = 789,
|
||||||
|
settings = {theme = "dark", lang = "en"},
|
||||||
|
cart = {items = {"item1", "item2"}, total = 25.99}
|
||||||
|
}
|
||||||
|
|
||||||
|
sessions.create(session_id, data, "persist_test")
|
||||||
|
sessions.close("persist_test")
|
||||||
|
|
||||||
|
-- Reinitialize and verify data persists
|
||||||
|
sessions.init("persist_test", "persist_sessions.json")
|
||||||
|
local retrieved = sessions.get(session_id, "persist_test")
|
||||||
|
|
||||||
|
assert_equal(789, retrieved.user_id)
|
||||||
|
assert_equal("dark", retrieved.settings.theme)
|
||||||
|
assert_equal("en", retrieved.settings.lang)
|
||||||
|
assert_equal(2, #retrieved.cart.items)
|
||||||
|
assert_equal(25.99, retrieved.cart.total)
|
||||||
|
|
||||||
|
sessions.close("persist_test")
|
||||||
|
os.remove("persist_sessions.json")
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- ======================================================================
|
||||||
|
-- COMPLEX DATA STRUCTURES
|
||||||
|
-- ======================================================================
|
||||||
|
|
||||||
|
test("Complex session data", function()
|
||||||
|
sessions.init("complex_test")
|
||||||
|
|
||||||
|
local session_id = sessions.generate_id()
|
||||||
|
local complex_data = {
|
||||||
|
user = {
|
||||||
|
id = 456,
|
||||||
|
profile = {
|
||||||
|
name = "Jane Doe",
|
||||||
|
email = "jane@example.com",
|
||||||
|
preferences = {
|
||||||
|
notifications = true,
|
||||||
|
privacy = "friends_only"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
activity = {
|
||||||
|
pages_visited = {"home", "profile", "settings"},
|
||||||
|
actions = {
|
||||||
|
{type = "login", time = os.time()},
|
||||||
|
{type = "view_page", page = "profile", time = os.time()}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
metadata = {
|
||||||
|
ip = "192.168.1.1",
|
||||||
|
user_agent = "Test Browser 1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sessions.create(session_id, complex_data)
|
||||||
|
local retrieved = sessions.get(session_id)
|
||||||
|
|
||||||
|
assert_equal(456, retrieved.user.id)
|
||||||
|
assert_equal("Jane Doe", retrieved.user.profile.name)
|
||||||
|
assert_equal(true, retrieved.user.profile.preferences.notifications)
|
||||||
|
assert_equal(3, #retrieved.activity.pages_visited)
|
||||||
|
assert_equal("login", retrieved.activity.actions[1].type)
|
||||||
|
assert_equal("192.168.1.1", retrieved.metadata.ip)
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- ======================================================================
|
||||||
|
-- WORKFLOW INTEGRATION
|
||||||
|
-- ======================================================================
|
||||||
|
|
||||||
|
test("Session workflow integration", function()
|
||||||
|
sessions.init("workflow_test")
|
||||||
|
|
||||||
|
-- Simulate user workflow
|
||||||
|
local session_id = sessions.generate_id()
|
||||||
|
|
||||||
|
-- User login
|
||||||
|
sessions.create(session_id, {
|
||||||
|
user_id = 999,
|
||||||
|
username = "workflow_user",
|
||||||
|
status = "logged_in"
|
||||||
|
})
|
||||||
|
|
||||||
|
-- User adds items to cart
|
||||||
|
local session = sessions.get(session_id)
|
||||||
|
session.cart = {"item1", "item2"}
|
||||||
|
session.cart_total = 19.99
|
||||||
|
sessions.update(session_id, session)
|
||||||
|
|
||||||
|
-- User proceeds to checkout
|
||||||
|
session = sessions.get(session_id)
|
||||||
|
session.checkout_step = "payment"
|
||||||
|
session.payment_method = "credit_card"
|
||||||
|
sessions.update(session_id, session)
|
||||||
|
|
||||||
|
-- Verify final state
|
||||||
|
local final_session = sessions.get(session_id)
|
||||||
|
assert_equal(999, final_session.user_id)
|
||||||
|
assert_equal("logged_in", final_session.status)
|
||||||
|
assert_equal(2, #final_session.cart)
|
||||||
|
assert_equal(19.99, final_session.cart_total)
|
||||||
|
assert_equal("payment", final_session.checkout_step)
|
||||||
|
assert_equal("credit_card", final_session.payment_method)
|
||||||
|
|
||||||
|
-- User completes order and logs out
|
||||||
|
sessions.delete(session_id)
|
||||||
|
assert_equal(nil, sessions.get(session_id))
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Clean up test files
|
||||||
|
--os.remove("test_sessions.json")
|
||||||
|
--os.remove("test_sessions2.json")
|
||||||
|
|
||||||
|
summary()
|
||||||
|
test_exit()
|
Loading…
x
Reference in New Issue
Block a user