feat: implement file system persistence with save and load functionality
This commit is contained in:
@@ -0,0 +1,62 @@
|
|||||||
|
package persistence
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/StevanFreeborn/term-tier/internal/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FileSystem interface {
|
||||||
|
WriteFile(name string, data []byte, perm os.FileMode) error
|
||||||
|
ReadFile(name string) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type osFileSystem struct{}
|
||||||
|
|
||||||
|
func (osFileSystem) WriteFile(name string, data []byte, perm os.FileMode) error {
|
||||||
|
return os.WriteFile(name, data, perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (osFileSystem) ReadFile(name string) ([]byte, error) {
|
||||||
|
return os.ReadFile(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
type fileSaver struct {
|
||||||
|
fs FileSystem
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFileSaver() core.Saver {
|
||||||
|
return &fileSaver{
|
||||||
|
fs: osFileSystem{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFileSaverWithFS(fs FileSystem) core.Saver {
|
||||||
|
return &fileSaver{
|
||||||
|
fs: fs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fileSaver) Save(filename string, state core.SaveState) error {
|
||||||
|
data, err := json.MarshalIndent(state, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.fs.WriteFile(filename, data, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fileSaver) Load(filename string) (core.SaveState, error) {
|
||||||
|
data, err := s.fs.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return core.SaveState{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var state core.SaveState
|
||||||
|
if err := json.Unmarshal(data, &state); err != nil {
|
||||||
|
return core.SaveState{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return state, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package persistence_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/StevanFreeborn/term-tier/internal/core"
|
||||||
|
"github.com/StevanFreeborn/term-tier/internal/persistence"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFileSaver_Save(t *testing.T) {
|
||||||
|
t.Run("Successfully saves content to mock FS", func(t *testing.T) {
|
||||||
|
mock := newMockFS()
|
||||||
|
saver := persistence.NewFileSaverWithFS(mock)
|
||||||
|
|
||||||
|
inputState := core.SaveState{}
|
||||||
|
|
||||||
|
filename := "test_tierlist.json"
|
||||||
|
err := saver.Save(filename, inputState)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := mock.files[filename]; !exists {
|
||||||
|
t.Error("Expected file to be created in mock FS")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Returns error on write failure", func(t *testing.T) {
|
||||||
|
mock := newMockFS()
|
||||||
|
mock.writeErr = errors.New("disk full")
|
||||||
|
saver := persistence.NewFileSaverWithFS(mock)
|
||||||
|
|
||||||
|
err := saver.Save("fail.json", core.SaveState{})
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error due to write failure, got nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileSaver_Load(t *testing.T) {
|
||||||
|
t.Run("Successfully loads existing file", func(t *testing.T) {
|
||||||
|
mock := newMockFS()
|
||||||
|
filename := "data.json"
|
||||||
|
|
||||||
|
validJSON := []byte(`{
|
||||||
|
"tiers": []
|
||||||
|
}`)
|
||||||
|
|
||||||
|
mock.files[filename] = validJSON
|
||||||
|
|
||||||
|
saver := persistence.NewFileSaverWithFS(mock)
|
||||||
|
result, err := saver.Load(filename)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if reflect.ValueOf(result).IsZero() && len(validJSON) == 0 {
|
||||||
|
t.Error("Loaded data does not match expected content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Returns error when file does not exist", func(t *testing.T) {
|
||||||
|
mock := newMockFS()
|
||||||
|
saver := persistence.NewFileSaverWithFS(mock)
|
||||||
|
|
||||||
|
_, err := saver.Load("nonexistent.json")
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error for missing file, got nil")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
t.Errorf("Expected os.ErrNotExist, got %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Returns error on invalid JSON", func(t *testing.T) {
|
||||||
|
mock := newMockFS()
|
||||||
|
mock.files["bad.json"] = []byte(`{ broken_json: `)
|
||||||
|
saver := persistence.NewFileSaverWithFS(mock)
|
||||||
|
|
||||||
|
_, err := saver.Load("bad.json")
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected JSON unmarshal error, got nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockFS struct {
|
||||||
|
files map[string][]byte
|
||||||
|
writeErr error
|
||||||
|
readErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockFS() *mockFS {
|
||||||
|
return &mockFS{
|
||||||
|
files: make(map[string][]byte),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockFS) WriteFile(name string, data []byte, perm os.FileMode) error {
|
||||||
|
if m.writeErr != nil {
|
||||||
|
return m.writeErr
|
||||||
|
}
|
||||||
|
m.files[name] = data
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockFS) ReadFile(name string) ([]byte, error) {
|
||||||
|
if m.readErr != nil {
|
||||||
|
return nil, m.readErr
|
||||||
|
}
|
||||||
|
data, exists := m.files[name]
|
||||||
|
if !exists {
|
||||||
|
return nil, os.ErrNotExist
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user