cmd/relui: add persistence to development database

This change renames memoryStore to fileStore, and persists data created
in the UI. This will help make local development less painful when
testing changes. Data stored is intended to be loaded at start-up, which
will be implemented in a future change.

For golang/go#40279

Co-authored-by: Carlos Amedee <carlos@golang.org>
Change-Id: Id2390c35b8e1d1d368fbf7ac13b3cdef0776ad87
Reviewed-on: https://go-review.googlesource.com/c/build/+/246298
Run-TryBot: Alexander Rakoczy <alex@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Reviewed-by: Andrew Bonventre <andybons@golang.org>
diff --git a/cmd/relui/main.go b/cmd/relui/main.go
index fced950..21c52db 100644
--- a/cmd/relui/main.go
+++ b/cmd/relui/main.go
@@ -6,6 +6,7 @@
 package main
 
 import (
+	"flag"
 	"io/ioutil"
 	"log"
 	"net/http"
@@ -16,8 +17,13 @@
 	reluipb "golang.org/x/build/cmd/relui/protos"
 )
 
+var (
+	devDataDir = flag.String("dev-data-directory", defaultDevDataDir(), "Development-only directory to use for storage of application state.")
+)
+
 func main() {
-	s := &server{store: &memoryStore{}, configs: loadWorkflowConfig("./workflows")}
+	flag.Parse()
+	s := &server{store: newFileStore(*devDataDir), configs: loadWorkflowConfig("./workflows")}
 	http.Handle("/workflows/create", http.HandlerFunc(s.createWorkflowHandler))
 	http.Handle("/workflows/new", http.HandlerFunc(s.newWorkflowHandler))
 	http.Handle("/", fileServerHandler(relativeFile("./static"), http.HandlerFunc(s.homeHandler)))
@@ -54,3 +60,12 @@
 	}
 	return ws
 }
+
+// defaultDevDataDir returns a directory suitable for storage of data when developing relui on most platforms.
+func defaultDevDataDir() string {
+	c, err := os.UserConfigDir()
+	if err != nil {
+		c = os.TempDir()
+	}
+	return filepath.Join(c, "go-build", "relui")
+}
diff --git a/cmd/relui/protos/relui.pb.go b/cmd/relui/protos/relui.pb.go
index 039aaa6..6cca73a 100644
--- a/cmd/relui/protos/relui.pb.go
+++ b/cmd/relui/protos/relui.pb.go
@@ -195,6 +195,47 @@
 	return ""
 }
 
+// LocalStorage is the persisted data of relui. It is used in development mode for saving application state.
+type LocalStorage struct {
+	// workflows are a list of user-created workflows.
+	Workflows            []*Workflow `protobuf:"bytes,1,rep,name=workflows,proto3" json:"workflows,omitempty"`
+	XXX_NoUnkeyedLiteral struct{}    `json:"-"`
+	XXX_unrecognized     []byte      `json:"-"`
+	XXX_sizecache        int32       `json:"-"`
+}
+
+func (m *LocalStorage) Reset()         { *m = LocalStorage{} }
+func (m *LocalStorage) String() string { return proto.CompactTextString(m) }
+func (*LocalStorage) ProtoMessage()    {}
+func (*LocalStorage) Descriptor() ([]byte, []int) {
+	return fileDescriptor_6de8859f82adce0a, []int{2}
+}
+
+func (m *LocalStorage) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_LocalStorage.Unmarshal(m, b)
+}
+func (m *LocalStorage) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_LocalStorage.Marshal(b, m, deterministic)
+}
+func (m *LocalStorage) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_LocalStorage.Merge(m, src)
+}
+func (m *LocalStorage) XXX_Size() int {
+	return xxx_messageInfo_LocalStorage.Size(m)
+}
+func (m *LocalStorage) XXX_DiscardUnknown() {
+	xxx_messageInfo_LocalStorage.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_LocalStorage proto.InternalMessageInfo
+
+func (m *LocalStorage) GetWorkflows() []*Workflow {
+	if m != nil {
+		return m.Workflows
+	}
+	return nil
+}
+
 type GitSource struct {
 	Url                  string   `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
 	Ref                  string   `protobuf:"bytes,2,opt,name=ref,proto3" json:"ref,omitempty"`
@@ -207,7 +248,7 @@
 func (m *GitSource) String() string { return proto.CompactTextString(m) }
 func (*GitSource) ProtoMessage()    {}
 func (*GitSource) Descriptor() ([]byte, []int) {
-	return fileDescriptor_6de8859f82adce0a, []int{2}
+	return fileDescriptor_6de8859f82adce0a, []int{3}
 }
 
 func (m *GitSource) XXX_Unmarshal(b []byte) error {
@@ -247,35 +288,38 @@
 	proto.RegisterType((*Workflow)(nil), "protos.Workflow")
 	proto.RegisterMapType((map[string]string)(nil), "protos.Workflow.ParamsEntry")
 	proto.RegisterType((*BuildableTask)(nil), "protos.BuildableTask")
+	proto.RegisterType((*LocalStorage)(nil), "protos.LocalStorage")
 	proto.RegisterType((*GitSource)(nil), "protos.GitSource")
 }
 
 func init() { proto.RegisterFile("relui.proto", fileDescriptor_6de8859f82adce0a) }
 
 var fileDescriptor_6de8859f82adce0a = []byte{
-	// 384 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x52, 0x4d, 0x6f, 0xda, 0x40,
-	0x10, 0xed, 0x1a, 0xb0, 0xf0, 0xb8, 0xa5, 0x74, 0xdb, 0xaa, 0x56, 0x3f, 0x24, 0xca, 0xc9, 0xe2,
-	0x40, 0x2b, 0xda, 0x43, 0x9b, 0x43, 0x24, 0x27, 0x41, 0x39, 0x20, 0x41, 0x64, 0x1b, 0x71, 0xb4,
-	0xd6, 0xb0, 0x20, 0xcb, 0x8b, 0x6d, 0xed, 0xae, 0x13, 0xf9, 0x57, 0xe6, 0x2f, 0xe4, 0xa7, 0x44,
-	0xbb, 0xd8, 0x09, 0x41, 0x39, 0xf9, 0xed, 0x7b, 0x33, 0x6f, 0xde, 0x8c, 0x0c, 0x36, 0xa7, 0xac,
-	0x4c, 0xc6, 0x05, 0xcf, 0x65, 0x8e, 0x4d, 0xfd, 0x11, 0xc3, 0x7b, 0x04, 0xdd, 0x55, 0xce, 0xd3,
-	0x2d, 0xcb, 0xef, 0x30, 0x86, 0x76, 0x46, 0xf6, 0xd4, 0x41, 0x03, 0xe4, 0x5a, 0xbe, 0xc6, 0xf8,
-	0x1c, 0xde, 0xc7, 0x65, 0xc2, 0x36, 0x24, 0x66, 0x34, 0x92, 0x44, 0xa4, 0xc2, 0x31, 0x06, 0x2d,
-	0xd7, 0x9e, 0x7c, 0x3e, 0x38, 0x89, 0xf1, 0x45, 0x23, 0x87, 0x44, 0xa4, 0x7e, 0x2f, 0x3e, 0x7e,
-	0x0a, 0xfc, 0x17, 0xcc, 0x82, 0x70, 0xb2, 0x17, 0x4e, 0x4b, 0xb7, 0x7d, 0x6f, 0xda, 0x9a, 0xa9,
-	0xe3, 0x1b, 0x2d, 0x4f, 0x33, 0xc9, 0x2b, 0xbf, 0xae, 0xfd, 0xfa, 0x1f, 0xec, 0x23, 0x1a, 0xf7,
-	0xa1, 0x95, 0xd2, 0xaa, 0xce, 0xa5, 0x20, 0xfe, 0x04, 0x9d, 0x5b, 0xc2, 0x4a, 0xea, 0x18, 0x9a,
-	0x3b, 0x3c, 0xce, 0x8c, 0x7f, 0x68, 0xf8, 0x80, 0xe0, 0xdd, 0x8b, 0x48, 0xaf, 0xae, 0xf5, 0x03,
-	0x60, 0x43, 0x0b, 0x9a, 0x6d, 0x44, 0x94, 0x67, 0xb5, 0x89, 0x55, 0x33, 0x8b, 0x0c, 0x8f, 0xc0,
-	0x14, 0x92, 0xc8, 0x52, 0xa5, 0x46, 0x6e, 0x6f, 0x82, 0x9b, 0xd4, 0xca, 0x30, 0xd0, 0x8a, 0x5f,
-	0x57, 0xe0, 0x9f, 0xf0, 0x96, 0x70, 0x99, 0x6c, 0xc9, 0x5a, 0x46, 0x25, 0x67, 0x4e, 0x5b, 0x9b,
-	0xd9, 0x0d, 0xb7, 0xe4, 0x0c, 0xff, 0x06, 0xd8, 0x25, 0x32, 0x12, 0x79, 0xc9, 0xd7, 0xd4, 0xe9,
-	0x0c, 0x90, 0x6b, 0x4f, 0x3e, 0x34, 0x96, 0xd7, 0x89, 0x0c, 0xb4, 0xe0, 0x5b, 0xbb, 0x06, 0xe2,
-	0x6f, 0x60, 0xa9, 0x63, 0x47, 0xb2, 0x2a, 0xa8, 0x63, 0x6a, 0xc7, 0xae, 0x22, 0xc2, 0xaa, 0xa0,
-	0xc3, 0x5f, 0x60, 0x3d, 0x35, 0xa9, 0xdb, 0xa8, 0xa9, 0xf5, 0x6d, 0x4a, 0xce, 0x14, 0xc3, 0xe9,
-	0xb6, 0x5e, 0x4a, 0xc1, 0xd1, 0x0a, 0xe0, 0x39, 0x38, 0xfe, 0x02, 0x1f, 0x43, 0x2f, 0x98, 0x45,
-	0x41, 0xe8, 0x85, 0xcb, 0x20, 0x5a, 0xce, 0x67, 0xf3, 0xc5, 0x6a, 0xde, 0x7f, 0x73, 0x2a, 0x5c,
-	0xfa, 0x53, 0x2f, 0x9c, 0x5e, 0xf5, 0xd1, 0xa9, 0x10, 0x84, 0x9e, 0xaf, 0x04, 0x23, 0x3e, 0xfc,
-	0x46, 0x7f, 0x1e, 0x03, 0x00, 0x00, 0xff, 0xff, 0x73, 0x67, 0x81, 0x13, 0x5c, 0x02, 0x00, 0x00,
+	// 411 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x52, 0x4f, 0x6f, 0xd3, 0x30,
+	0x1c, 0xc5, 0xed, 0x56, 0x2d, 0xbf, 0x8c, 0x51, 0x0c, 0x08, 0x8b, 0x3f, 0x52, 0xe9, 0x29, 0xda,
+	0x21, 0xa0, 0xc2, 0x01, 0x38, 0x4c, 0x0a, 0x50, 0x71, 0x18, 0xea, 0x90, 0x93, 0xaa, 0xc7, 0xc8,
+	0x69, 0xdd, 0x2a, 0x8a, 0x17, 0x47, 0xb6, 0xc3, 0x94, 0x4f, 0xc9, 0x57, 0xe0, 0xa3, 0x20, 0xbb,
+	0x0e, 0x1b, 0xd5, 0x4e, 0x79, 0x7e, 0xef, 0xe7, 0xe7, 0xf7, 0x7b, 0x0a, 0x84, 0x8a, 0x8b, 0xb6,
+	0x8c, 0x1b, 0x25, 0x8d, 0xc4, 0x23, 0xf7, 0xd1, 0xd3, 0xdf, 0x08, 0x4e, 0x56, 0x52, 0x55, 0x5b,
+	0x21, 0x6f, 0x30, 0x86, 0xa3, 0x9a, 0x5d, 0x73, 0x82, 0x26, 0x28, 0x0a, 0xa8, 0xc3, 0xf8, 0x02,
+	0x1e, 0x15, 0x6d, 0x29, 0x36, 0xac, 0x10, 0x3c, 0x37, 0x4c, 0x57, 0x9a, 0x0c, 0x26, 0xc3, 0x28,
+	0x9c, 0x3d, 0xdb, 0x3b, 0xe9, 0xf8, 0x4b, 0x2f, 0x67, 0x4c, 0x57, 0xf4, 0xac, 0xb8, 0x7b, 0xd4,
+	0xf8, 0x03, 0x8c, 0x1a, 0xa6, 0xd8, 0xb5, 0x26, 0x43, 0x77, 0xed, 0x55, 0x7f, 0xad, 0x7f, 0x35,
+	0xfe, 0xe9, 0xe4, 0x79, 0x6d, 0x54, 0x47, 0xfd, 0xec, 0x8b, 0x4f, 0x10, 0xde, 0xa1, 0xf1, 0x18,
+	0x86, 0x15, 0xef, 0x7c, 0x2e, 0x0b, 0xf1, 0x53, 0x38, 0xfe, 0xc5, 0x44, 0xcb, 0xc9, 0xc0, 0x71,
+	0xfb, 0xc3, 0xe7, 0xc1, 0x47, 0x34, 0xfd, 0x83, 0xe0, 0xe1, 0x7f, 0x91, 0xee, 0x5d, 0xeb, 0x35,
+	0xc0, 0x86, 0x37, 0xbc, 0xde, 0xe8, 0x5c, 0xd6, 0xde, 0x24, 0xf0, 0xcc, 0x55, 0x8d, 0xcf, 0x61,
+	0xa4, 0x0d, 0x33, 0xad, 0x4d, 0x8d, 0xa2, 0xb3, 0x19, 0xee, 0x53, 0x5b, 0xc3, 0xd4, 0x29, 0xd4,
+	0x4f, 0xe0, 0x37, 0x70, 0xca, 0x94, 0x29, 0xb7, 0x6c, 0x6d, 0xf2, 0x56, 0x09, 0x72, 0xe4, 0xcc,
+	0xc2, 0x9e, 0x5b, 0x2a, 0x81, 0xdf, 0x01, 0xec, 0x4a, 0x93, 0x6b, 0xd9, 0xaa, 0x35, 0x27, 0xc7,
+	0x13, 0x14, 0x85, 0xb3, 0xc7, 0xbd, 0xe5, 0xf7, 0xd2, 0xa4, 0x4e, 0xa0, 0xc1, 0xae, 0x87, 0xf8,
+	0x25, 0x04, 0xb6, 0xec, 0xdc, 0x74, 0x0d, 0x27, 0x23, 0xe7, 0x78, 0x62, 0x89, 0xac, 0x6b, 0xf8,
+	0xf4, 0x02, 0x4e, 0x7f, 0xc8, 0x35, 0x13, 0xa9, 0x91, 0x8a, 0xed, 0x38, 0x8e, 0x21, 0xb8, 0xf1,
+	0x6d, 0x6a, 0x82, 0x5c, 0xcd, 0xe3, 0xc3, 0x9a, 0xe9, 0xed, 0xc8, 0xf4, 0x2d, 0x04, 0xff, 0x1e,
+	0xb5, 0xdd, 0xda, 0xd4, 0xbe, 0xdb, 0x56, 0x09, 0xcb, 0x28, 0xbe, 0xf5, 0xa5, 0x58, 0x78, 0xbe,
+	0x02, 0xb8, 0x5d, 0x1c, 0x3f, 0x87, 0x27, 0x59, 0x92, 0x5e, 0xe6, 0x69, 0x96, 0x64, 0xcb, 0x34,
+	0x5f, 0x2e, 0x2e, 0x17, 0x57, 0xab, 0xc5, 0xf8, 0xc1, 0xa1, 0xf0, 0x95, 0xce, 0x93, 0x6c, 0xfe,
+	0x6d, 0x8c, 0x0e, 0x85, 0x34, 0x4b, 0xa8, 0x15, 0x06, 0xc5, 0xfe, 0x37, 0x7c, 0xff, 0x37, 0x00,
+	0x00, 0xff, 0xff, 0xfa, 0x0a, 0x66, 0x8f, 0x9c, 0x02, 0x00, 0x00,
 }
diff --git a/cmd/relui/protos/relui.proto b/cmd/relui/protos/relui.proto
index fa1be06..6060dfa 100644
--- a/cmd/relui/protos/relui.proto
+++ b/cmd/relui/protos/relui.proto
@@ -41,6 +41,12 @@
   string task_type = 6;
 }
 
+// LocalStorage is the persisted data of relui. It is used in development mode for saving application state.
+message LocalStorage {
+  // workflows are a list of user-created workflows.
+  repeated Workflow workflows = 1;
+}
+
 message GitSource {
   string url = 1;
   string ref = 2;
diff --git a/cmd/relui/store.go b/cmd/relui/store.go
index 09fd362..a861058 100644
--- a/cmd/relui/store.go
+++ b/cmd/relui/store.go
@@ -5,38 +5,81 @@
 package main
 
 import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
 	"sync"
 
+	"github.com/golang/protobuf/proto"
 	reluipb "golang.org/x/build/cmd/relui/protos"
 )
 
 // store is a persistence adapter for saving data.
 type store interface {
-	GetWorkflows() []*reluipb.Workflow
+	Workflows() []*reluipb.Workflow
 	AddWorkflow(workflow *reluipb.Workflow) error
 }
 
-var _ store = (*memoryStore)(nil)
+var _ store = (*fileStore)(nil)
 
-// memoryStore is a non-durable implementation of store that keeps everything in memory.
-type memoryStore struct {
-	mut       sync.Mutex
-	workflows []*reluipb.Workflow
+// newFileStore initializes a fileStore ready for use.
+//
+// If dir is set to an empty string (""), no data will be saved to disk.
+func newFileStore(dir string) *fileStore {
+	return &fileStore{
+		persistDir: dir,
+		ls:         new(reluipb.LocalStorage),
+	}
 }
 
-// AddWorkflow adds a workflow to the store.
-func (m *memoryStore) AddWorkflow(w *reluipb.Workflow) error {
-	m.mut.Lock()
-	defer m.mut.Unlock()
-	m.workflows = append(m.workflows, w)
+// fileStoreName is the name of the data file used by fileStore for persistence.
+const fileStoreName = "local_storage.textpb"
+
+// fileStore is a non-durable implementation of store that keeps everything in memory.
+type fileStore struct {
+	mu sync.Mutex
+	ls *reluipb.LocalStorage
+
+	// persistDir is a path to a directory for saving application data in textproto format.
+	persistDir string
+}
+
+// AddWorkflow adds a workflow to the store, persisting changes to disk.
+func (f *fileStore) AddWorkflow(w *reluipb.Workflow) error {
+	f.mu.Lock()
+	f.ls.Workflows = append(f.ls.Workflows, w)
+	f.mu.Unlock()
+	if err := f.persist(); err != nil {
+		return err
+	}
 	return nil
 }
 
-// GetWorkflows returns all workflows stored.
-//
-// TODO(golang.org/issue/40279) - clone workflows if they're ever mutated.
-func (m *memoryStore) GetWorkflows() []*reluipb.Workflow {
-	m.mut.Lock()
-	defer m.mut.Unlock()
-	return m.workflows
+// Workflows returns all workflows stored.
+func (f *fileStore) Workflows() []*reluipb.Workflow {
+	return f.localStorage().GetWorkflows()
+}
+
+// localStorage returns a deep copy of data stored in fileStore.
+func (f *fileStore) localStorage() *reluipb.LocalStorage {
+	f.mu.Lock()
+	defer f.mu.Unlock()
+	return proto.Clone(f.ls).(*reluipb.LocalStorage)
+}
+
+// persist saves fileStore state to persistDir/fileStoreName.
+func (f *fileStore) persist() error {
+	if f.persistDir == "" {
+		return nil
+	}
+	if err := os.MkdirAll(f.persistDir, 0755); err != nil {
+		return fmt.Errorf("os.MkDirAll(%q, %v) = %w", f.persistDir, 0755, err)
+	}
+	dst := filepath.Join(f.persistDir, fileStoreName)
+	data := []byte(proto.MarshalTextString(f.localStorage()))
+	if err := ioutil.WriteFile(dst, data, 0644); err != nil {
+		return fmt.Errorf("ioutil.WriteFile(%q, _, %v) = %w", dst, 0644, err)
+	}
+	return nil
 }
diff --git a/cmd/relui/store_test.go b/cmd/relui/store_test.go
new file mode 100644
index 0000000..79843db
--- /dev/null
+++ b/cmd/relui/store_test.go
@@ -0,0 +1,52 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/golang/protobuf/proto"
+	"github.com/google/go-cmp/cmp"
+	reluipb "golang.org/x/build/cmd/relui/protos"
+)
+
+func TestFileStorePersist(t *testing.T) {
+	dir, err := ioutil.TempDir("", "memory-store-test")
+	if err != nil {
+		t.Fatalf("ioutil.TempDir(%q, %q) = _, %v", "", "memory-store-test", err)
+	}
+	defer os.RemoveAll(dir)
+	want := &reluipb.LocalStorage{
+		Workflows: []*reluipb.Workflow{
+			{
+				Name:           "Persist Test",
+				BuildableTasks: []*reluipb.BuildableTask{{Name: "Persist Test Task"}},
+			},
+		},
+	}
+	fs := newFileStore(filepath.Join(dir, "relui"))
+	fs.ls = want
+
+	err = fs.persist()
+	if err != nil {
+		t.Fatalf("fs.Persist() = %v, wanted no error", err)
+	}
+
+	b, err := ioutil.ReadFile(filepath.Join(dir, "relui", fileStoreName))
+	if err != nil {
+		t.Fatalf("ioutil.ReadFile(%q) = _, %v, wanted no error", filepath.Join(dir, "relui", fileStoreName), err)
+	}
+	got := new(reluipb.LocalStorage)
+	err = proto.UnmarshalText(string(b), got)
+	if err != nil {
+		t.Fatalf("proto.UnmarshalText(_) = %v, wanted no error", err)
+	}
+	if diff := cmp.Diff(want, got); diff != "" {
+		t.Errorf("reluipb.LocalStorage mismatch (-want, +got):\n%s", diff)
+	}
+}
diff --git a/cmd/relui/web.go b/cmd/relui/web.go
index fddde4c..a20d2fd 100644
--- a/cmd/relui/web.go
+++ b/cmd/relui/web.go
@@ -64,7 +64,7 @@
 // homeHandler renders the homepage.
 func (s *server) homeHandler(w http.ResponseWriter, _ *http.Request) {
 	out := bytes.Buffer{}
-	if err := homeTmpl.Execute(&out, homeResponse{Workflows: s.store.GetWorkflows()}); err != nil {
+	if err := homeTmpl.Execute(&out, homeResponse{Workflows: s.store.Workflows()}); err != nil {
 		log.Printf("homeHandler: %v", err)
 		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
 		return
@@ -106,6 +106,7 @@
 	}
 	wf.Params["GitObject"] = ref
 	if err := s.store.AddWorkflow(wf); err != nil {
+		log.Printf("Error adding workflow: s.store.AddWorkflow(%v) = %v", wf, err)
 		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
 		return
 	}
diff --git a/cmd/relui/web_test.go b/cmd/relui/web_test.go
index 311a30b..ab1ae59 100644
--- a/cmd/relui/web_test.go
+++ b/cmd/relui/web_test.go
@@ -83,7 +83,7 @@
 	req := httptest.NewRequest(http.MethodGet, "/", nil)
 	w := httptest.NewRecorder()
 
-	s := &server{store: &memoryStore{}}
+	s := &server{store: newFileStore("")}
 	s.homeHandler(w, req)
 	resp := w.Result()
 
@@ -96,7 +96,7 @@
 	req := httptest.NewRequest(http.MethodGet, "/workflows/new", nil)
 	w := httptest.NewRecorder()
 
-	s := &server{store: &memoryStore{}}
+	s := &server{store: newFileStore("")}
 	s.newWorkflowHandler(w, req)
 	resp := w.Result()
 
@@ -134,7 +134,7 @@
 			req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 			w := httptest.NewRecorder()
 
-			s := &server{store: &memoryStore{}, configs: config}
+			s := &server{store: newFileStore(""), configs: config}
 			s.createWorkflowHandler(w, req)
 			resp := w.Result()
 
@@ -146,16 +146,16 @@
 					t.Errorf("resp.Header.Get(%q) = %q, wanted %q", k, resp.Header.Get(k), v)
 				}
 			}
-			if len(s.store.GetWorkflows()) != 1 && c.wantParams != nil {
-				t.Fatalf("len(s.store.GetWorkflows()) = %d, wanted %d", len(s.store.GetWorkflows()), 1)
-			} else if len(s.store.GetWorkflows()) != 0 && c.wantParams == nil {
-				t.Fatalf("len(s.store.GetWorkflows()) = %d, wanted %d", len(s.store.GetWorkflows()), 0)
+			if len(s.store.Workflows()) != 1 && c.wantParams != nil {
+				t.Fatalf("len(s.store.Workflows()) = %d, wanted %d", len(s.store.Workflows()), 1)
+			} else if len(s.store.Workflows()) != 0 && c.wantParams == nil {
+				t.Fatalf("len(s.store.Workflows()) = %d, wanted %d", len(s.store.Workflows()), 0)
 			}
 			if c.wantParams == nil {
 				return
 			}
-			if diff := cmp.Diff(c.wantParams, s.store.GetWorkflows()[0].GetParams()); diff != "" {
-				t.Errorf("s.Store.GetWorkflows()[0].Params() mismatch (-want, +got):\n%s", diff)
+			if diff := cmp.Diff(c.wantParams, s.store.Workflows()[0].GetParams()); diff != "" {
+				t.Errorf("s.Store.Workflows()[0].Params() mismatch (-want, +got):\n%s", diff)
 			}
 		})
 	}