package core import ( "fmt" "slices" "testing" "time" _ "github.com/mattn/go-sqlite3" ) func TestCreateSource(t *testing.T) { db := EphemeralDb(t) if err := AddSource(db, "one"); err != nil { t.Fatal(err) } if err := AddSource(db, "two"); err != nil { t.Fatal(err) } if err := AddSource(db, "three"); err != nil { t.Fatal(err) } if err := DeleteSource(db, "two"); err != nil { t.Fatal(err) } names, err := GetSources(db) if err != nil { t.Fatal(err) } expected := []string{"one", "three"} for i := 0; i < len(expected); i += 1 { if !slices.Contains(names, expected[i]) { t.Fatalf("missing %s, have: %v", expected[i], names) } } } func AssertItemIs(t *testing.T, item Item, expected string) { actual := fmt.Sprintf( "%s/%s/%t/%s/%s/%s/%s/%d", item.Source, item.Id, item.Active, item.Title, item.Author, item.Body, item.Link, item.Time, ) if actual != expected { t.Fatalf("expected %s, got %s", expected, actual) } } func TestAddItem(t *testing.T) { db := EphemeralDb(t) if err := AddSource(db, "test"); err != nil { t.Fatalf("failed to add source: %v", err) } if err := AddItems(db, []Item{ {"test", "one", 0, true, "", "", "", "", 0}, {"test", "two", 0, true, "title", "author", "body", "link", 123456}, }); err != nil { t.Fatalf("failed to add items: %v", err) } items, err := GetActiveItemsForSource(db, "test") if err != nil { t.Fatalf("failed to get active items: %v", err) } if len(items) != 2 { t.Fatal("should get two items") } AssertItemIs(t, items[0], "test/one/true/////0") AssertItemIs(t, items[1], "test/two/true/title/author/body/link/123456") if _, err = DeactivateItem(db, "test", "one"); err != nil { t.Fatal(err) } items, err = GetActiveItemsForSource(db, "test") if err != nil { t.Fatal(err) } if len(items) != 1 { t.Fatal("should get one item") } items, err = GetAllItemsForSource(db, "test") if err != nil { t.Fatal(err) } if len(items) != 2 { t.Fatal("should get two items") } deleted, err := DeleteItem(db, "test", "one") if err != nil { t.Fatal(err) } if deleted != 1 { t.Fatal("expected one deletion") } deleted, err = DeleteItem(db, "test", "one") if err != nil { t.Fatal(err) } if deleted != 0 { t.Fatal("expected no deletion") } items, err = GetAllItemsForSource(db, "test") if err != nil { t.Fatal(err) } if len(items) != 1 { t.Fatal("should get one item") } } func TestUpdateSourceAddAndDelete(t *testing.T) { db := EphemeralDb(t) if err := AddSource(db, "test"); err != nil { t.Fatal(err) } a := Item{Source: "test", Id: "a"} add, del, err := UpdateWithFetchedItems(db, "test", []Item{a}) if add != 1 || del != 0 || err != nil { t.Fatalf("update failed: add %d, del %d, err %v", add, del, err) } add, del, err = UpdateWithFetchedItems(db, "test", []Item{a}) if add != 0 || del != 0 || err != nil { t.Fatalf("update failed: add %d, del %d, err %v", add, del, err) } b := Item{Source: "test", Id: "b"} add, del, err = UpdateWithFetchedItems(db, "test", []Item{a, b}) if add != 1 || del != 0 || err != nil { t.Fatalf("update failed: add %d, del %d, err %v", add, del, err) } if _, err = DeactivateItem(db, "test", "a"); err != nil { t.Fatal(err) } add, del, err = UpdateWithFetchedItems(db, "test", []Item{a, b}) if add != 0 || del != 0 || err != nil { t.Fatalf("update failed: add %d, del %d, err %v", add, del, err) } add, del, err = UpdateWithFetchedItems(db, "test", []Item{b}) if add != 0 || del != 1 || err != nil { t.Fatalf("update failed: add %d, del %d, err %v", add, del, err) } add, del, err = UpdateWithFetchedItems(db, "test", []Item{b}) if add != 0 || del != 0 || err != nil { t.Fatalf("update failed: add %d, del %d, err %v", add, del, err) } } func TestOnCreateAction(t *testing.T) { db := EphemeralDb(t) if err := AddSource(db, "test"); err != nil { t.Fatal(err) } if err := AddAction(db, "test", "on_create", []string{"true"}); err != nil { t.Fatal(err) } execute := func(argv []string) []Item { items, err := Execute("test", argv, nil, "", time.Minute) if err != nil { t.Fatal("unexpected error executing test fetch") } if len(items) != 1 { t.Fatalf("expected only one item, got %d", len(items)) } return items } onCreate := func(argv []string) { if err := UpdateAction(db, "test", "on_create", argv); err != nil { t.Fatal(err) } } getItem := func(id string) Item { item, err := GetItem(db, "test", id) if err != nil { t.Fatal(err) } return item } // Noop on_create works onCreate([]string{"tee"}) items := execute([]string{"jq", "-cn", `{id: "one"}`}) add, _, err := UpdateWithFetchedItems(db, "test", items) if add != 1 || err != nil { t.Fatal("failed update with noop oncreate") } updated := getItem("one") updated.Created = 0 // zero out for comparison with pre-insert item if !ItemsAreEqual(updated, items[0]) { t.Fatalf("expected no change: %#v != %#v", updated, items[0]) } // on_create can change a field onCreate([]string{"jq", "-c", `.title = "Goodbye, World"`}) items = execute([]string{"jq", "-cn", `{id: "two", title: "Hello, World"}`}) if items[0].Title != "Hello, World" { t.Fatal("unexpected title") } add, _, err = UpdateWithFetchedItems(db, "test", items) if add != 1 || err != nil { t.Fatal("failed update with alter oncreate") } if getItem("two").Title != "Goodbye, World" { t.Fatal("title not updated") } // on_create can add a field onCreate([]string{"jq", "-c", `.link = "gopher://go.dev"`}) items = execute([]string{"jq", "-cn", `{id: "three"}`}) if items[0].Link != "" { t.Fatal("unexpected link") } add, _, err = UpdateWithFetchedItems(db, "test", items) if add != 1 || err != nil { t.Fatal("failed update with augment oncreate") } if getItem("three").Link != "gopher://go.dev" { t.Fatal("link not added") } // on_create can't delete a field using a zero value // due to zero values preserving prior field values onCreate([]string{"jq", "-c", `del(.link)`}) items = execute([]string{"jq", "-cn", `{id: "four", link: "gopher://go.dev"}`}) if items[0].Link != "gopher://go.dev" { t.Fatal("missing link") } add, _, err = UpdateWithFetchedItems(db, "test", items) if add != 1 || err != nil { t.Fatal("failed update with attempted deletion oncreate") } if getItem("four").Link != "gopher://go.dev" { t.Fatal("link unexpectedly removed") } // item is created if on_create fails onCreate([]string{"false"}) items = execute([]string{"jq", "-cn", `{id: "five"}`}) add, _, err = UpdateWithFetchedItems(db, "test", items) if add != 1 || err != nil { t.Fatal("failed update with failing oncreate") } if getItem("five").Id != "five" { t.Fatal("item not created") } // item isn't updated if on_create has valid output but a bad exit code onCreate([]string{"sh", "-c", `jq -cn '{id: "six", title: "after"}'; exit 1`}) items = execute([]string{"jq", "-cn", `{id: "six", title: "before"}`}) if items[0].Title != "before" { t.Fatal("unexpected title") } add, _, err = UpdateWithFetchedItems(db, "test", items) if add != 1 || err != nil { t.Fatal("failed update with bad exit code oncreate") } if getItem("six").Title != "before" { t.Fatal("update applied after oncreate failed") } // on_create can't change id, active, or created onCreate([]string{"jq", "-c", `.id = "seven"; .active = false; .created = 123456`}) items = execute([]string{"jq", "-cn", `{id: "seven"}`}) add, _, err = UpdateWithFetchedItems(db, "test", items) if add != 1 || err != nil { t.Fatal("failed update with invalid field changes oncreate") } updated = getItem("seven") if updated.Id != "seven" || !updated.Active || updated.Created == 123456 { t.Fatal("unexpected changes to id, active, or created fields") } }