Diffdown is a real-time collaborative Markdown editor/previewer built on the AT Protocol
diffdown.com
1package collaboration
2
3import (
4 "os"
5 "path/filepath"
6 "testing"
7 "time"
8
9 "github.com/limeleaf/diffdown/internal/db"
10 "github.com/limeleaf/diffdown/internal/model"
11)
12
13// openTestDB opens a temporary SQLite DB with migrations applied.
14func openTestDB(t *testing.T) *db.DB {
15 t.Helper()
16 tmp := t.TempDir()
17 database, err := db.Open(filepath.Join(tmp, "test.db"))
18 if err != nil {
19 t.Fatalf("open test db: %v", err)
20 }
21 // Point migrations at the real migrations directory (relative to module root).
22 db.SetMigrationsDir(filepath.Join("..", "..", "migrations"))
23 if err := database.Migrate(); err != nil {
24 t.Fatalf("migrate: %v", err)
25 }
26 t.Cleanup(func() {
27 database.Close()
28 os.RemoveAll(tmp)
29 })
30 return database
31}
32
33// --- GenerateInviteToken ---
34
35func TestGenerateInviteToken_Length(t *testing.T) {
36 token, err := GenerateInviteToken()
37 if err != nil {
38 t.Fatalf("unexpected error: %v", err)
39 }
40 // SHA-256 hex = 64 chars
41 if len(token) != 64 {
42 t.Errorf("expected token length 64, got %d", len(token))
43 }
44}
45
46func TestGenerateInviteToken_Unique(t *testing.T) {
47 t1, err1 := GenerateInviteToken()
48 t2, err2 := GenerateInviteToken()
49 if err1 != nil || err2 != nil {
50 t.Fatalf("unexpected errors: %v %v", err1, err2)
51 }
52 if t1 == t2 {
53 t.Error("expected unique tokens, got duplicates")
54 }
55}
56
57// --- CreateInvite ---
58
59func TestCreateInvite_ReturnsInvite(t *testing.T) {
60 database := openTestDB(t)
61 invite, err := CreateInvite(database, "rkey123", "did:plc:creator")
62 if err != nil {
63 t.Fatalf("CreateInvite: %v", err)
64 }
65 if invite.ID == "" {
66 t.Error("expected non-empty ID")
67 }
68 if invite.Token == "" {
69 t.Error("expected non-empty Token")
70 }
71 if invite.DocumentRKey != "rkey123" {
72 t.Errorf("DocumentRKey: got %q, want %q", invite.DocumentRKey, "rkey123")
73 }
74 if invite.CreatedBy != "did:plc:creator" {
75 t.Errorf("CreatedBy: got %q, want %q", invite.CreatedBy, "did:plc:creator")
76 }
77}
78
79func TestCreateInvite_ExpiresInSevenDays(t *testing.T) {
80 database := openTestDB(t)
81 before := time.Now()
82 invite, err := CreateInvite(database, "rkey123", "did:plc:creator")
83 if err != nil {
84 t.Fatalf("CreateInvite: %v", err)
85 }
86 after := time.Now()
87
88 minExpiry := before.Add(7 * 24 * time.Hour)
89 maxExpiry := after.Add(7 * 24 * time.Hour)
90
91 if invite.ExpiresAt.Before(minExpiry) || invite.ExpiresAt.After(maxExpiry) {
92 t.Errorf("ExpiresAt %v not in expected range [%v, %v]", invite.ExpiresAt, minExpiry, maxExpiry)
93 }
94}
95
96func TestCreateInvite_PersistedInDB(t *testing.T) {
97 database := openTestDB(t)
98 invite, err := CreateInvite(database, "rkey123", "did:plc:creator")
99 if err != nil {
100 t.Fatalf("CreateInvite: %v", err)
101 }
102
103 fetched, err := database.GetInviteByToken(invite.Token)
104 if err != nil {
105 t.Fatalf("GetInviteByToken: %v", err)
106 }
107 if fetched.DocumentRKey != invite.DocumentRKey {
108 t.Errorf("persisted DocumentRKey: got %q, want %q", fetched.DocumentRKey, invite.DocumentRKey)
109 }
110}
111
112// --- ValidateInvite ---
113
114func TestValidateInvite_ValidToken(t *testing.T) {
115 database := openTestDB(t)
116 invite, err := CreateInvite(database, "rkey123", "did:plc:creator")
117 if err != nil {
118 t.Fatalf("CreateInvite: %v", err)
119 }
120
121 validated, err := ValidateInvite(database, invite.Token, "rkey123")
122 if err != nil {
123 t.Fatalf("ValidateInvite: %v", err)
124 }
125 if validated.Token != invite.Token {
126 t.Errorf("got token %q, want %q", validated.Token, invite.Token)
127 }
128}
129
130func TestValidateInvite_WrongRKey(t *testing.T) {
131 database := openTestDB(t)
132 invite, err := CreateInvite(database, "rkey123", "did:plc:creator")
133 if err != nil {
134 t.Fatalf("CreateInvite: %v", err)
135 }
136
137 _, err = ValidateInvite(database, invite.Token, "wrong-rkey")
138 if err == nil {
139 t.Error("expected error for mismatched rkey, got nil")
140 }
141}
142
143func TestValidateInvite_UnknownToken(t *testing.T) {
144 database := openTestDB(t)
145 _, err := ValidateInvite(database, "nonexistent-token", "rkey123")
146 if err == nil {
147 t.Error("expected error for unknown token, got nil")
148 }
149}
150
151func TestValidateInvite_ExpiredToken(t *testing.T) {
152 database := openTestDB(t)
153
154 // Insert an already-expired invite directly.
155 expired := &model.Invite{
156 ID: db.NewID(),
157 DocumentRKey: "rkey123",
158 Token: "expiredtoken1234567890abcdef1234567890abcdef1234567890abcdef12",
159 CreatedBy: "did:plc:creator",
160 CreatedAt: time.Now().Add(-8 * 24 * time.Hour),
161 ExpiresAt: time.Now().Add(-1 * time.Hour),
162 }
163 if err := database.CreateInvite(expired); err != nil {
164 t.Fatalf("CreateInvite expired: %v", err)
165 }
166
167 _, err := ValidateInvite(database, expired.Token, "rkey123")
168 if err == nil {
169 t.Error("expected error for expired token, got nil")
170 }
171}