code.pablomurad.com

patchwork

Publishes the revisions of your pages as a feed, with notes and readable diffs, written in Go

internal/store/store_test.go raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
package store

import (
	"context"
	"errors"
	"path/filepath"
	"testing"
	"time"
)

func open(t *testing.T) *Store {
	t.Helper()
	s, err := Open(filepath.Join(t.TempDir(), "test.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { s.Close() })
	return s
}

var t0 = time.Date(2026, 10, 21, 22, 14, 3, 0, time.UTC)

func TestRoundTrip(t *testing.T) {
	ctx := context.Background()
	s := open(t)

	p := &Page{UID: NewUID(), Source: "folder", Path: "a.md", Slug: "a", OriginalDate: "2026-06-21", FirstSeenAt: t0}
	if err := s.InsertPage(ctx, p); err != nil {
		t.Fatal(err)
	}
	snap := &Snapshot{PageID: p.ID, ObservedAt: t0, RawHash: "r1", ContentHash: "c1", Raw: "x", FrontmatterJSON: "{}", Note: "n"}
	if err := s.InsertSnapshot(ctx, snap); err != nil {
		t.Fatal(err)
	}
	snap2 := &Snapshot{PageID: p.ID, ObservedAt: t0.Add(time.Hour), RawHash: "r2", ContentHash: "c2", Raw: "y", FrontmatterJSON: "{}"}
	if err := s.InsertSnapshot(ctx, snap2); err != nil {
		t.Fatal(err)
	}
	if snap.Seq != 1 || snap2.Seq != 2 {
		t.Fatalf("seq %d %d", snap.Seq, snap2.Seq)
	}
	last, err := s.LastSnapshot(ctx, p.ID)
	if err != nil || last.RawHash != "r2" || !last.ObservedAt.Equal(t0.Add(time.Hour)) {
		t.Fatalf("last snapshot %+v %v", last, err)
	}

	base := &Revision{PageID: p.ID, Number: 1, SnapshotID: snap.ID, PublishedAt: t0, UpdatedAt: t0}
	pub := &Revision{PageID: p.ID, Number: 2, SnapshotID: snap2.ID, Note: "changed", Kind: "position", PublishedAt: t0.Add(time.Hour), UpdatedAt: t0.Add(time.Hour)}
	for _, r := range []*Revision{base, pub} {
		if err := s.InsertRevision(ctx, r); err != nil {
			t.Fatal(err)
		}
	}
	lr, err := s.LastRevision(ctx, p.ID)
	if err != nil || lr.Number != 2 || lr.Note != "changed" {
		t.Fatalf("last revision %+v %v", lr, err)
	}
	recent, err := s.RecentRevisions(ctx, 10, time.Time{})
	if err != nil || len(recent) != 1 || recent[0].Number != 2 || recent[0].ID != pub.ID {
		t.Fatalf("recent %+v %v", recent, err)
	}

	now := t0.Add(2 * time.Hour)
	pub.RetractedAt = &now
	if err := s.UpdateRevision(ctx, pub); err != nil {
		t.Fatal(err)
	}
	if lr, _ := s.LastRevision(ctx, p.ID); lr.Number != 1 {
		t.Fatalf("retracted revision still last: %d", lr.Number)
	}
	if max, _ := s.MaxRevisionNumber(ctx, p.ID); max != 2 {
		t.Fatalf("max %d", max)
	}
	if recent, _ := s.RecentRevisions(ctx, 10, time.Time{}); len(recent) != 0 {
		t.Fatal("retracted revisions must leave the feed")
	}
}

func TestLookupsAndTx(t *testing.T) {
	ctx := context.Background()
	s := open(t)
	if _, err := s.PageByPath(ctx, "folder", "missing.md"); !errors.Is(err, ErrNotFound) {
		t.Fatalf("want ErrNotFound, got %v", err)
	}

	// A dry-run transaction leaves nothing behind.
	err := s.Tx(ctx, true, func(q *Queries) error {
		return q.InsertPage(ctx, &Page{UID: NewUID(), Source: "folder", Path: "dry.md", Slug: "dry", FirstSeenAt: t0})
	})
	if err != nil {
		t.Fatal(err)
	}
	if pages, _ := s.Pages(ctx, "", true); len(pages) != 0 {
		t.Fatal("dry run committed")
	}

	if err := s.SetMeta(ctx, "k", "v1"); err != nil {
		t.Fatal(err)
	}
	s.SetMeta(ctx, "k", "v2")
	if v, _ := s.Meta(ctx, "k"); v != "v2" {
		t.Fatalf("meta %q", v)
	}
}

func TestReopenKeepsSchema(t *testing.T) {
	path := filepath.Join(t.TempDir(), "re.db")
	s, err := Open(path)
	if err != nil {
		t.Fatal(err)
	}
	s.Close()
	s, err = Open(path)
	if err != nil {
		t.Fatal(err)
	}
	defer s.Close()
	if err := s.Backup(context.Background(), filepath.Join(filepath.Dir(path), "copy.db")); err != nil {
		t.Fatal(err)
	}
}