carlos / todomvc-bakeoff Public

Clone
git clone https://amadan.net/carlos/todomvc-bakeoff

Plain git — no account needed to clone.

Download

Download this file

1package todo
2
3import (
4 "context"
5 "errors"
6 "path/filepath"
7 "strings"
8 "testing"
9)
10
11func open(t *testing.T) *Store {
12 t.Helper()
13 s, err := Open(context.Background(), filepath.Join(t.TempDir(), "todo.db"))
14 if err != nil {
15 t.Fatal(err)
16 }
17 t.Cleanup(func() { s.Close() })
18 return s
19}
20
21func titles(t *testing.T, s *Store, k ListKey) string {
22 t.Helper()
23 items, err := s.List(context.Background(), k)
24 if err != nil {
25 t.Fatal(err)
26 }
27 var b strings.Builder
28 for _, it := range items {
29 if it.Completed {
30 b.WriteString("x:")
31 }
32 b.WriteString(it.Title + ";")
33 }
34 return b.String()
35}
36
37func TestAddTrimsAndSkipsBlank(t *testing.T) {
38 s, ctx, k := open(t), context.Background(), KeyFor([]byte("a"))
39 for _, title := range []string{" one ", " ", "", "two"} {
40 if err := s.Add(ctx, k, title); err != nil {
41 t.Fatal(err)
42 }
43 }
44 if got := titles(t, s, k); got != "one;two;" {
45 t.Fatalf("got %q", got)
46 }
47}
48
49func TestToggleAllAndClear(t *testing.T) {
50 s, ctx, k := open(t), context.Background(), KeyFor([]byte("a"))
51 for _, title := range []string{"a", "b", "c"} {
52 s.Add(ctx, k, title)
53 }
54 items, _ := s.List(ctx, k)
55 if err := s.Toggle(ctx, k, items[0].ID); err != nil {
56 t.Fatal(err)
57 }
58 s.ToggleAll(ctx, k) // some active: all become completed
59 if got := titles(t, s, k); got != "x:a;x:b;x:c;" {
60 t.Fatalf("after first toggle-all: %q", got)
61 }
62 s.ToggleAll(ctx, k) // all completed: all become active
63 if got := titles(t, s, k); got != "a;b;c;" {
64 t.Fatalf("after second toggle-all: %q", got)
65 }
66 s.Toggle(ctx, k, items[1].ID)
67 s.ClearCompleted(ctx, k)
68 if got := titles(t, s, k); got != "a;c;" {
69 t.Fatalf("after clear: %q", got)
70 }
71}
72
73func TestRenameEmptyDeletes(t *testing.T) {
74 s, ctx, k := open(t), context.Background(), KeyFor([]byte("a"))
75 s.Add(ctx, k, "a")
76 s.Add(ctx, k, "b")
77 items, _ := s.List(ctx, k)
78 if err := s.Rename(ctx, k, items[0].ID, " renamed "); err != nil {
79 t.Fatal(err)
80 }
81 if err := s.Rename(ctx, k, items[1].ID, " "); err != nil {
82 t.Fatal(err)
83 }
84 if got := titles(t, s, k); got != "renamed;" {
85 t.Fatalf("got %q", got)
86 }
87}
88
89// Another list's id must look exactly like a missing one, and change nothing.
90func TestListsAreIsolated(t *testing.T) {
91 s, ctx := open(t), context.Background()
92 mine, theirs := KeyFor([]byte("mine")), KeyFor([]byte("theirs"))
93 s.Add(ctx, mine, "private")
94 items, _ := s.List(ctx, mine)
95 id := items[0].ID
96
97 for name, err := range map[string]error{
98 "toggle": s.Toggle(ctx, theirs, id),
99 "rename": s.Rename(ctx, theirs, id, "hacked"),
100 "empty": s.Rename(ctx, theirs, id, ""),
101 "delete": s.Delete(ctx, theirs, id),
102 } {
103 if !errors.Is(err, ErrNotFound) {
104 t.Errorf("%s: got %v, want ErrNotFound", name, err)
105 }
106 }
107 s.ToggleAll(ctx, theirs)
108 s.ClearCompleted(ctx, theirs)
109 if got := titles(t, s, mine); got != "private;" {
110 t.Fatalf("my list changed: %q", got)
111 }
112 if got := titles(t, s, theirs); got != "" {
113 t.Fatalf("their list sees: %q", got)
114 }
115}
116
117func TestKeepsUnicodeAndLongTitles(t *testing.T) {
118 s, ctx, k := open(t), context.Background(), KeyFor([]byte("a"))
119 long := strings.Repeat("x", 10_000)
120 s.Add(ctx, k, "naïve 🚀 日本語")
121 s.Add(ctx, k, long)
122 items, _ := s.List(ctx, k)
123 if items[0].Title != "naïve 🚀 日本語" || items[1].Title != long {
124 t.Fatalf("titles changed: %q, %d chars", items[0].Title, len(items[1].Title))
125 }
126}
127
128// Reopening an existing database runs no migration twice and keeps data.
129func TestReopenKeepsData(t *testing.T) {
130 path := filepath.Join(t.TempDir(), "todo.db")
131 ctx, k := context.Background(), KeyFor([]byte("a"))
132 s, err := Open(ctx, path)
133 if err != nil {
134 t.Fatal(err)
135 }
136 s.Add(ctx, k, "kept")
137 s.Close()
138 s, err = Open(ctx, path)
139 if err != nil {
140 t.Fatal(err)
141 }
142 defer s.Close()
143 if got := titles(t, s, k); got != "kept;" {
144 t.Fatalf("got %q", got)
145 }
146 var mode string
147 s.r.QueryRow(`PRAGMA journal_mode`).Scan(&mode)
148 if mode != "wal" {
149 t.Fatalf("journal_mode = %q, want wal", mode)
150 }
151}
152
153// A file an earlier build left behind, with its own tables and
154// user_version, opens cleanly and keeps that data untouched.
155func TestOpensBesideForeignTables(t *testing.T) {
156 path := filepath.Join(t.TempDir(), "todo.db")
157 ctx := context.Background()
158 s, err := Open(ctx, path)
159 if err != nil {
160 t.Fatal(err)
161 }
162 for _, q := range []string{
163 `DROP TABLE todo_items`, `DROP TABLE todo_lists`, `DROP TABLE todo_migrations`,
164 `CREATE TABLE todos (id INTEGER PRIMARY KEY, text TEXT)`,
165 `INSERT INTO todos (text) VALUES ('old')`,
166 `PRAGMA user_version = 7`,
167 } {
168 if _, err := s.w.Exec(q); err != nil {
169 t.Fatal(err)
170 }
171 }
172 s.Close()
173
174 s, err = Open(ctx, path)
175 if err != nil {
176 t.Fatal(err)
177 }
178 defer s.Close()
179 k := KeyFor([]byte("a"))
180 s.Add(ctx, k, "new")
181 if got := titles(t, s, k); got != "new;" {
182 t.Fatalf("got %q", got)
183 }
184 var old string
185 s.r.QueryRow(`SELECT text FROM todos`).Scan(&old)
186 if old != "old" {
187 t.Fatalf("earlier data lost: %q", old)
188 }
189}
190
191func TestOnlyIssuedListsExist(t *testing.T) {
192 s, ctx := open(t), context.Background()
193 k := KeyFor([]byte("issued"))
194 if ok, _ := s.ListExists(ctx, k); ok {
195 t.Fatal("unissued list exists")
196 }
197 if err := s.CreateList(ctx, k); err != nil {
198 t.Fatal(err)
199 }
200 if ok, _ := s.ListExists(ctx, k); !ok {
201 t.Fatal("issued list missing")
202 }
203}
204
205// Lists that already held todos before the lists table existed keep working.
206func TestListsMigrationKeepsExistingLists(t *testing.T) {
207 path := filepath.Join(t.TempDir(), "todo.db")
208 ctx, k := context.Background(), KeyFor([]byte("old visitor"))
209 s, err := Open(ctx, path)
210 if err != nil {
211 t.Fatal(err)
212 }
213 s.Add(ctx, k, "kept")
214 for _, q := range []string{`DROP TABLE todo_lists`, `DELETE FROM todo_migrations WHERE version = 2`} {
215 if _, err := s.w.Exec(q); err != nil {
216 t.Fatal(err)
217 }
218 }
219 s.Close()
220 s, err = Open(ctx, path)
221 if err != nil {
222 t.Fatal(err)
223 }
224 defer s.Close()
225 if ok, _ := s.ListExists(ctx, k); !ok {
226 t.Fatal("existing list not carried into todo_lists")
227 }
228}
229