cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists ๐Ÿƒ
charm leaflet readability golang

build: tests for publication commands

+615
+253
cmd/publication_commands_test.go
··· 1 + package main 2 + 3 + import ( 4 + "context" 5 + "strings" 6 + "testing" 7 + 8 + "github.com/stormlightlabs/noteleaf/internal/handlers" 9 + ) 10 + 11 + func createTestPublicationHandler(t *testing.T) (*handlers.PublicationHandler, func()) { 12 + cleanup := setupCommandTest(t) 13 + handler, err := handlers.NewPublicationHandler() 14 + if err != nil { 15 + cleanup() 16 + t.Fatalf("Failed to create test publication handler: %v", err) 17 + } 18 + return handler, func() { 19 + handler.Close() 20 + cleanup() 21 + } 22 + } 23 + 24 + func TestPublicationCommand(t *testing.T) { 25 + t.Run("CommandGroup Interface", func(t *testing.T) { 26 + handler, cleanup := createTestPublicationHandler(t) 27 + defer cleanup() 28 + 29 + var _ CommandGroup = NewPublicationCommand(handler) 30 + }) 31 + 32 + t.Run("Create", func(t *testing.T) { 33 + t.Run("creates command with correct structure", func(t *testing.T) { 34 + handler, cleanup := createTestPublicationHandler(t) 35 + defer cleanup() 36 + 37 + cmd := NewPublicationCommand(handler).Create() 38 + 39 + if cmd == nil { 40 + t.Fatal("Create returned nil") 41 + } 42 + if cmd.Use != "pub" { 43 + t.Errorf("Expected Use to be 'pub', got '%s'", cmd.Use) 44 + } 45 + if cmd.Short != "Manage leaflet publication sync" { 46 + t.Errorf("Expected Short to be 'Manage leaflet publication sync', got '%s'", cmd.Short) 47 + } 48 + if !cmd.HasSubCommands() { 49 + t.Error("Expected command to have subcommands") 50 + } 51 + }) 52 + 53 + t.Run("has all expected subcommands", func(t *testing.T) { 54 + handler, cleanup := createTestPublicationHandler(t) 55 + defer cleanup() 56 + 57 + cmd := NewPublicationCommand(handler).Create() 58 + subcommands := cmd.Commands() 59 + subcommandNames := make([]string, len(subcommands)) 60 + for i, subcmd := range subcommands { 61 + subcommandNames[i] = subcmd.Use 62 + } 63 + 64 + expectedSubcommands := []string{ 65 + "auth [handle]", 66 + "pull", 67 + "list [--published|--draft|--all]", 68 + "status", 69 + } 70 + 71 + for _, expected := range expectedSubcommands { 72 + if !findSubcommand(subcommandNames, expected) { 73 + t.Errorf("Expected subcommand '%s' not found in %v", expected, subcommandNames) 74 + } 75 + } 76 + }) 77 + }) 78 + 79 + t.Run("Status Command", func(t *testing.T) { 80 + t.Run("shows not authenticated initially", func(t *testing.T) { 81 + handler, cleanup := createTestPublicationHandler(t) 82 + defer cleanup() 83 + 84 + cmd := NewPublicationCommand(handler).Create() 85 + cmd.SetArgs([]string{"status"}) 86 + err := cmd.Execute() 87 + 88 + if err != nil { 89 + t.Errorf("status command failed: %v", err) 90 + } 91 + }) 92 + }) 93 + 94 + t.Run("List Command", func(t *testing.T) { 95 + t.Run("default filter", func(t *testing.T) { 96 + handler, cleanup := createTestPublicationHandler(t) 97 + defer cleanup() 98 + 99 + cmd := NewPublicationCommand(handler).Create() 100 + cmd.SetArgs([]string{"list"}) 101 + err := cmd.Execute() 102 + 103 + if err != nil { 104 + t.Errorf("list command failed: %v", err) 105 + } 106 + }) 107 + 108 + t.Run("with published flag", func(t *testing.T) { 109 + handler, cleanup := createTestPublicationHandler(t) 110 + defer cleanup() 111 + 112 + cmd := NewPublicationCommand(handler).Create() 113 + cmd.SetArgs([]string{"list", "--published"}) 114 + err := cmd.Execute() 115 + 116 + if err != nil { 117 + t.Errorf("list --published failed: %v", err) 118 + } 119 + }) 120 + 121 + t.Run("with draft flag", func(t *testing.T) { 122 + handler, cleanup := createTestPublicationHandler(t) 123 + defer cleanup() 124 + 125 + cmd := NewPublicationCommand(handler).Create() 126 + cmd.SetArgs([]string{"list", "--draft"}) 127 + err := cmd.Execute() 128 + 129 + if err != nil { 130 + t.Errorf("list --draft failed: %v", err) 131 + } 132 + }) 133 + 134 + t.Run("with all flag", func(t *testing.T) { 135 + handler, cleanup := createTestPublicationHandler(t) 136 + defer cleanup() 137 + 138 + cmd := NewPublicationCommand(handler).Create() 139 + cmd.SetArgs([]string{"list", "--all"}) 140 + err := cmd.Execute() 141 + 142 + if err != nil { 143 + t.Errorf("list --all failed: %v", err) 144 + } 145 + }) 146 + 147 + t.Run("published takes precedence over draft", func(t *testing.T) { 148 + handler, cleanup := createTestPublicationHandler(t) 149 + defer cleanup() 150 + 151 + cmd := NewPublicationCommand(handler).Create() 152 + cmd.SetArgs([]string{"list", "--published", "--draft"}) 153 + err := cmd.Execute() 154 + 155 + if err != nil { 156 + t.Errorf("list with multiple flags failed: %v", err) 157 + } 158 + }) 159 + }) 160 + 161 + t.Run("Pull Command", func(t *testing.T) { 162 + t.Run("fails when not authenticated", func(t *testing.T) { 163 + handler, cleanup := createTestPublicationHandler(t) 164 + defer cleanup() 165 + 166 + cmd := NewPublicationCommand(handler).Create() 167 + cmd.SetArgs([]string{"pull"}) 168 + err := cmd.Execute() 169 + 170 + if err == nil { 171 + t.Error("Expected pull to fail when not authenticated") 172 + } 173 + if err != nil && !strings.Contains(err.Error(), "not authenticated") { 174 + t.Errorf("Expected 'not authenticated' error, got: %v", err) 175 + } 176 + }) 177 + }) 178 + 179 + t.Run("Command Help", func(t *testing.T) { 180 + t.Run("root help", func(t *testing.T) { 181 + handler, cleanup := createTestPublicationHandler(t) 182 + defer cleanup() 183 + 184 + cmd := NewPublicationCommand(handler).Create() 185 + cmd.SetArgs([]string{"help"}) 186 + err := cmd.Execute() 187 + 188 + if err != nil { 189 + t.Errorf("help command failed: %v", err) 190 + } 191 + }) 192 + 193 + t.Run("auth help", func(t *testing.T) { 194 + handler, cleanup := createTestPublicationHandler(t) 195 + defer cleanup() 196 + 197 + cmd := NewPublicationCommand(handler).Create() 198 + cmd.SetArgs([]string{"auth", "--help"}) 199 + err := cmd.Execute() 200 + 201 + if err != nil { 202 + t.Errorf("auth help failed: %v", err) 203 + } 204 + }) 205 + }) 206 + 207 + t.Run("Command Aliases", func(t *testing.T) { 208 + t.Run("list alias ls works", func(t *testing.T) { 209 + handler, cleanup := createTestPublicationHandler(t) 210 + defer cleanup() 211 + 212 + cmd := NewPublicationCommand(handler).Create() 213 + cmd.SetArgs([]string{"ls"}) 214 + err := cmd.Execute() 215 + 216 + if err != nil { 217 + t.Errorf("list alias 'ls' failed: %v", err) 218 + } 219 + }) 220 + }) 221 + 222 + t.Run("Handler Validation", func(t *testing.T) { 223 + t.Run("auth validates empty handle", func(t *testing.T) { 224 + handler, cleanup := createTestPublicationHandler(t) 225 + defer cleanup() 226 + 227 + ctx := context.Background() 228 + err := handler.Auth(ctx, "", "password") 229 + 230 + if err == nil { 231 + t.Error("Expected error for empty handle") 232 + } 233 + if !strings.Contains(err.Error(), "handle is required") { 234 + t.Errorf("Expected 'handle is required' error, got: %v", err) 235 + } 236 + }) 237 + 238 + t.Run("auth validates empty password", func(t *testing.T) { 239 + handler, cleanup := createTestPublicationHandler(t) 240 + defer cleanup() 241 + 242 + ctx := context.Background() 243 + err := handler.Auth(ctx, "test.bsky.social", "") 244 + 245 + if err == nil { 246 + t.Error("Expected error for empty password") 247 + } 248 + if !strings.Contains(err.Error(), "password is required") { 249 + t.Errorf("Expected 'password is required' error, got: %v", err) 250 + } 251 + }) 252 + }) 253 + }
+362
internal/services/atproto_test.go
··· 2 2 3 3 import ( 4 4 "context" 5 + "strings" 5 6 "testing" 6 7 "time" 7 8 ··· 1075 1076 1076 1077 if err != nil && err.Error() == "not authenticated" { 1077 1078 t.Error("Authentication check should pass, but got authentication error") 1079 + } 1080 + }) 1081 + }) 1082 + 1083 + t.Run("Session Management Edge Cases", func(t *testing.T) { 1084 + t.Run("GetSession returns distinct error for nil session", func(t *testing.T) { 1085 + svc := NewATProtoService() 1086 + 1087 + session, err := svc.GetSession() 1088 + if err == nil { 1089 + t.Error("Expected error when getting nil session") 1090 + } 1091 + if session != nil { 1092 + t.Error("Expected nil session when not authenticated") 1093 + } 1094 + expectedMsg := "not authenticated" 1095 + if !strings.Contains(err.Error(), expectedMsg) { 1096 + t.Errorf("Expected error message to contain '%s', got '%v'", expectedMsg, err) 1097 + } 1098 + }) 1099 + 1100 + t.Run("RestoreSession validates all required fields", func(t *testing.T) { 1101 + svc := NewATProtoService() 1102 + 1103 + testCases := []struct { 1104 + name string 1105 + session *Session 1106 + }{ 1107 + { 1108 + name: "missing DID", 1109 + session: &Session{ 1110 + DID: "", 1111 + Handle: "test.bsky.social", 1112 + AccessJWT: "access", 1113 + RefreshJWT: "refresh", 1114 + }, 1115 + }, 1116 + { 1117 + name: "missing AccessJWT", 1118 + session: &Session{ 1119 + DID: "did:plc:test", 1120 + Handle: "test.bsky.social", 1121 + AccessJWT: "", 1122 + RefreshJWT: "refresh", 1123 + }, 1124 + }, 1125 + { 1126 + name: "missing RefreshJWT", 1127 + session: &Session{ 1128 + DID: "did:plc:test", 1129 + Handle: "test.bsky.social", 1130 + AccessJWT: "access", 1131 + RefreshJWT: "", 1132 + }, 1133 + }, 1134 + } 1135 + 1136 + for _, tc := range testCases { 1137 + t.Run(tc.name, func(t *testing.T) { 1138 + err := svc.RestoreSession(tc.session) 1139 + if err == nil { 1140 + t.Errorf("Expected error for %s", tc.name) 1141 + } 1142 + if !strings.Contains(err.Error(), "session missing required fields") { 1143 + t.Errorf("Expected 'session missing required fields' error, got: %v", err) 1144 + } 1145 + }) 1146 + } 1147 + }) 1148 + 1149 + t.Run("RestoreSession preserves empty PDSURL", func(t *testing.T) { 1150 + svc := NewATProtoService() 1151 + defaultPDSURL := svc.pdsURL 1152 + 1153 + session := &Session{ 1154 + DID: "did:plc:test123", 1155 + Handle: "test.bsky.social", 1156 + AccessJWT: "access_token", 1157 + RefreshJWT: "refresh_token", 1158 + PDSURL: "", 1159 + } 1160 + 1161 + err := svc.RestoreSession(session) 1162 + if err != nil { 1163 + t.Errorf("Expected no error, got %v", err) 1164 + } 1165 + 1166 + if svc.pdsURL != defaultPDSURL { 1167 + t.Errorf("Expected pdsURL to remain default when session PDSURL is empty, got '%s'", svc.pdsURL) 1168 + } 1169 + }) 1170 + }) 1171 + 1172 + t.Run("PostDocument Validation", func(t *testing.T) { 1173 + t.Run("validates title before marshaling", func(t *testing.T) { 1174 + svc := NewATProtoService() 1175 + svc.session = &Session{ 1176 + DID: "did:plc:test123", 1177 + Handle: "test.bsky.social", 1178 + AccessJWT: "access_token", 1179 + RefreshJWT: "refresh_token", 1180 + Authenticated: true, 1181 + } 1182 + ctx := context.Background() 1183 + 1184 + doc := public.Document{ 1185 + Title: "", 1186 + } 1187 + 1188 + result, err := svc.PostDocument(ctx, doc, false) 1189 + if err == nil { 1190 + t.Error("Expected error when title is empty") 1191 + } 1192 + if result != nil { 1193 + t.Error("Expected nil result when validation fails") 1194 + } 1195 + if !strings.Contains(err.Error(), "document title is required") { 1196 + t.Errorf("Expected 'document title is required' error, got: %v", err) 1197 + } 1198 + }) 1199 + 1200 + t.Run("sets correct collection for draft", func(t *testing.T) { 1201 + svc := NewATProtoService() 1202 + svc.session = &Session{ 1203 + DID: "did:plc:test123", 1204 + Handle: "test.bsky.social", 1205 + AccessJWT: "access_token", 1206 + RefreshJWT: "refresh_token", 1207 + Authenticated: true, 1208 + } 1209 + ctx := context.Background() 1210 + 1211 + doc := public.Document{ 1212 + Title: "Test Draft", 1213 + } 1214 + 1215 + _, err := svc.PostDocument(ctx, doc, true) 1216 + 1217 + if err != nil && strings.Contains(err.Error(), "document title is required") { 1218 + t.Error("Title validation should pass") 1219 + } 1220 + }) 1221 + 1222 + t.Run("sets correct collection for published", func(t *testing.T) { 1223 + svc := NewATProtoService() 1224 + svc.session = &Session{ 1225 + DID: "did:plc:test123", 1226 + Handle: "test.bsky.social", 1227 + AccessJWT: "access_token", 1228 + RefreshJWT: "refresh_token", 1229 + Authenticated: true, 1230 + } 1231 + ctx := context.Background() 1232 + 1233 + doc := public.Document{ 1234 + Title: "Test Published", 1235 + } 1236 + 1237 + _, err := svc.PostDocument(ctx, doc, false) 1238 + 1239 + if err != nil && strings.Contains(err.Error(), "document title is required") { 1240 + t.Error("Title validation should pass") 1241 + } 1242 + }) 1243 + }) 1244 + 1245 + t.Run("PatchDocument Validation", func(t *testing.T) { 1246 + t.Run("validates rkey before title", func(t *testing.T) { 1247 + svc := NewATProtoService() 1248 + svc.session = &Session{ 1249 + DID: "did:plc:test123", 1250 + Handle: "test.bsky.social", 1251 + AccessJWT: "access_token", 1252 + RefreshJWT: "refresh_token", 1253 + Authenticated: true, 1254 + } 1255 + ctx := context.Background() 1256 + 1257 + doc := public.Document{ 1258 + Title: "Valid Title", 1259 + } 1260 + 1261 + result, err := svc.PatchDocument(ctx, "", doc, false) 1262 + if err == nil { 1263 + t.Error("Expected error when rkey is empty") 1264 + } 1265 + if result != nil { 1266 + t.Error("Expected nil result when rkey validation fails") 1267 + } 1268 + if !strings.Contains(err.Error(), "rkey is required") { 1269 + t.Errorf("Expected 'rkey is required' error, got: %v", err) 1270 + } 1271 + }) 1272 + 1273 + t.Run("validates title after rkey", func(t *testing.T) { 1274 + svc := NewATProtoService() 1275 + svc.session = &Session{ 1276 + DID: "did:plc:test123", 1277 + Handle: "test.bsky.social", 1278 + AccessJWT: "access_token", 1279 + RefreshJWT: "refresh_token", 1280 + Authenticated: true, 1281 + } 1282 + ctx := context.Background() 1283 + 1284 + doc := public.Document{ 1285 + Title: "", 1286 + } 1287 + 1288 + result, err := svc.PatchDocument(ctx, "valid-rkey", doc, false) 1289 + if err == nil { 1290 + t.Error("Expected error when title is empty") 1291 + } 1292 + if result != nil { 1293 + t.Error("Expected nil result when title validation fails") 1294 + } 1295 + if !strings.Contains(err.Error(), "document title is required") { 1296 + t.Errorf("Expected 'document title is required' error, got: %v", err) 1297 + } 1298 + }) 1299 + 1300 + t.Run("sets correct collection for draft", func(t *testing.T) { 1301 + svc := NewATProtoService() 1302 + svc.session = &Session{ 1303 + DID: "did:plc:test123", 1304 + Handle: "test.bsky.social", 1305 + AccessJWT: "access_token", 1306 + RefreshJWT: "refresh_token", 1307 + Authenticated: true, 1308 + } 1309 + ctx := context.Background() 1310 + 1311 + doc := public.Document{ 1312 + Title: "Test Draft", 1313 + } 1314 + 1315 + _, err := svc.PatchDocument(ctx, "test-rkey", doc, true) 1316 + 1317 + if err != nil && strings.Contains(err.Error(), "document title is required") { 1318 + t.Error("Title validation should pass") 1319 + } 1320 + }) 1321 + 1322 + t.Run("sets correct collection for published", func(t *testing.T) { 1323 + svc := NewATProtoService() 1324 + svc.session = &Session{ 1325 + DID: "did:plc:test123", 1326 + Handle: "test.bsky.social", 1327 + AccessJWT: "access_token", 1328 + RefreshJWT: "refresh_token", 1329 + Authenticated: true, 1330 + } 1331 + ctx := context.Background() 1332 + 1333 + doc := public.Document{ 1334 + Title: "Test Published", 1335 + } 1336 + 1337 + _, err := svc.PatchDocument(ctx, "test-rkey", doc, false) 1338 + 1339 + if err != nil && strings.Contains(err.Error(), "document title is required") { 1340 + t.Error("Title validation should pass") 1341 + } 1342 + }) 1343 + }) 1344 + 1345 + t.Run("DeleteDocument Validation", func(t *testing.T) { 1346 + t.Run("validates rkey before attempting delete", func(t *testing.T) { 1347 + svc := NewATProtoService() 1348 + svc.session = &Session{ 1349 + DID: "did:plc:test123", 1350 + Handle: "test.bsky.social", 1351 + AccessJWT: "access_token", 1352 + RefreshJWT: "refresh_token", 1353 + Authenticated: true, 1354 + } 1355 + ctx := context.Background() 1356 + 1357 + err := svc.DeleteDocument(ctx, "", false) 1358 + if err == nil { 1359 + t.Error("Expected error when rkey is empty") 1360 + } 1361 + if !strings.Contains(err.Error(), "rkey is required") { 1362 + t.Errorf("Expected 'rkey is required' error, got: %v", err) 1363 + } 1364 + }) 1365 + 1366 + t.Run("uses correct collection for draft", func(t *testing.T) { 1367 + svc := NewATProtoService() 1368 + svc.session = &Session{ 1369 + DID: "did:plc:test123", 1370 + Handle: "test.bsky.social", 1371 + AccessJWT: "access_token", 1372 + RefreshJWT: "refresh_token", 1373 + Authenticated: true, 1374 + } 1375 + ctx := context.Background() 1376 + 1377 + err := svc.DeleteDocument(ctx, "test-rkey", true) 1378 + 1379 + if err != nil && strings.Contains(err.Error(), "rkey is required") { 1380 + t.Error("Rkey validation should pass") 1381 + } 1382 + }) 1383 + 1384 + t.Run("uses correct collection for published", func(t *testing.T) { 1385 + svc := NewATProtoService() 1386 + svc.session = &Session{ 1387 + DID: "did:plc:test123", 1388 + Handle: "test.bsky.social", 1389 + AccessJWT: "access_token", 1390 + RefreshJWT: "refresh_token", 1391 + Authenticated: true, 1392 + } 1393 + ctx := context.Background() 1394 + 1395 + err := svc.DeleteDocument(ctx, "test-rkey", false) 1396 + 1397 + if err != nil && strings.Contains(err.Error(), "rkey is required") { 1398 + t.Error("Rkey validation should pass") 1399 + } 1400 + }) 1401 + }) 1402 + 1403 + t.Run("Concurrent Operations", func(t *testing.T) { 1404 + t.Run("Close can be called multiple times", func(t *testing.T) { 1405 + svc := NewATProtoService() 1406 + svc.session = &Session{ 1407 + Handle: "test.bsky.social", 1408 + Authenticated: true, 1409 + } 1410 + 1411 + err1 := svc.Close() 1412 + if err1 != nil { 1413 + t.Errorf("First close should succeed: %v", err1) 1414 + } 1415 + 1416 + err2 := svc.Close() 1417 + if err2 != nil { 1418 + t.Errorf("Second close should succeed: %v", err2) 1419 + } 1420 + }) 1421 + 1422 + t.Run("IsAuthenticated after Close returns false", func(t *testing.T) { 1423 + svc := NewATProtoService() 1424 + svc.session = &Session{ 1425 + Handle: "test.bsky.social", 1426 + Authenticated: true, 1427 + } 1428 + 1429 + if !svc.IsAuthenticated() { 1430 + t.Error("Expected IsAuthenticated to return true before close") 1431 + } 1432 + 1433 + err := svc.Close() 1434 + if err != nil { 1435 + t.Errorf("Close failed: %v", err) 1436 + } 1437 + 1438 + if svc.IsAuthenticated() { 1439 + t.Error("Expected IsAuthenticated to return false after close") 1078 1440 } 1079 1441 }) 1080 1442 })