From 7f26740f4c4905cad6b71ea749b83fa5a51c1a0f Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 22 May 2025 23:41:54 +0000 Subject: feat: Add OPML import functionality via CLI This change introduces the ability to import feeds from an OPML file using a command-line interface. Key features: - New `--import-opml` (or `-I`) flag in `main.go` to specify the OPML file path. - The `importer.ImportOPML` function in `importer/importer.go` handles the parsing of the OPML file (using `github.com/gilliek/go-opml`) and addition of new feeds to the database. - Recursive processing of OPML outlines allows for importing feeds nested within folders. - Feeds are identified by their XML URL; existing feeds with the same URL are skipped to avoid duplicates. - Title extraction prioritizes the `title` attribute, then the `text` attribute of an outline, and falls back to "Untitled Feed". Comprehensive unit tests have been added in `importer/importer_test.go` to verify: - Correct parsing and importing of various OPML structures. - Proper handling of duplicate feeds (skipping). - Correct title extraction logic. - Database interaction and cleanup. --- importer/importer_test.go | 108 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 importer/importer_test.go (limited to 'importer/importer_test.go') diff --git a/importer/importer_test.go b/importer/importer_test.go new file mode 100644 index 0000000..f2c3927 --- /dev/null +++ b/importer/importer_test.go @@ -0,0 +1,108 @@ +package importer_test + +import ( + "testing" + "os" + "log" + "adammathes.com/neko/importer" + "adammathes.com/neko/models" + "adammathes.com/neko/models/feed" + "adammathes.com/neko/config" +) + +func TestImportOPML(t *testing.T) { + // a. Initialize Configuration and Test Database + config.Init("") // Load default configurations + originalDBFile := config.Config.DBFile + config.Config.DBFile = "test_opml_import.db" + + // Remove any pre-existing test database file to ensure a clean state + os.Remove(config.Config.DBFile) + + models.InitDB() // Initialize the database, creating test_opml_import.db + + defer func() { + // Attempt to remove the test database file. + err := os.Remove(config.Config.DBFile) + if err != nil { + log.Printf("Error removing test database: %v", err) + } + // Restore the original DB file path in the config. + config.Config.DBFile = originalDBFile + }() + + // b. Create Sample OPML File + opmlContent := ` + + Test Feeds + + + + + + + + + + +` + opmlFilePath := "test_opml.xml" + err := os.WriteFile(opmlFilePath, []byte(opmlContent), 0644) + if err != nil { + t.Fatalf("Failed to write test OPML file: %v", err) + } + defer os.Remove(opmlFilePath) + + // c. Pre-populate a feed (for testing skip logic) + preFeed := feed.Feed{Url: "http://example.com/feed2.xml", Title: "Pre-existing Feed 2", WebUrl: "http://example.com/feed2_pre.html"} + if err := preFeed.Create(); err != nil { + t.Fatalf("Failed to pre-populate feed: %v", err) + } + + // d. Call importer.ImportOPML(opmlFilePath) + importer.ImportOPML(opmlFilePath) + + // e. Verify Results + verifyFeed := func(expectedURL, expectedTitle, expectedHTMLURL string) { + t.Helper() + f := feed.Feed{} + // Use the GetByUrl method which is what the importer uses (via ByUrl) + // to check for existence. + dbFeed, err := f.ByUrl(expectedURL) + if err != nil { + t.Errorf("Failed to find feed %s: %v", expectedURL, err) + return + } + if dbFeed == nil || dbFeed.Id == 0 { + t.Errorf("Feed %s not found in database", expectedURL) + return + } + if dbFeed.Title != expectedTitle { + t.Errorf("For feed %s, expected title '%s', got '%s'", expectedURL, expectedTitle, dbFeed.Title) + } + if dbFeed.WebUrl != expectedHTMLURL { + t.Errorf("For feed %s, expected HTML URL '%s', got '%s'", expectedURL, expectedHTMLURL, dbFeed.WebUrl) + } + } + + verifyFeed("http://example.com/feed1.xml", "Feed 1 Title", "http://example.com/feed1.html") + verifyFeed("http://example.com/feed2.xml", "Pre-existing Feed 2", "http://example.com/feed2_pre.html") // Should not be overwritten + verifyFeed("http://example.com/feed3.xml", "Feed 3 Title", "http://example.com/feed3.html") + verifyFeed("http://example.com/feed4.xml", "Feed 4 Title Only", "http://example.com/feed4.html") + verifyFeed("http://example.com/feed5.xml", "Feed 5 Text Only", "http://example.com/feed5.html") + verifyFeed("http://example.com/feed6.xml", "Untitled Feed", "http://example.com/feed6.html") + + + allFeeds, err := feed.All() + if err != nil { + t.Fatalf("Failed to query all feeds: %v", err) + } + // Expected: feed1, pre-existing feed2, feed3, feed4, feed5, feed6 (Untitled) + expectedFeedCount := 6 + if len(allFeeds) != expectedFeedCount { + t.Errorf("Expected %d feeds in the database, got %d", expectedFeedCount, len(allFeeds)) + for _, f := range allFeeds { + t.Logf("Found feed: %s (%s)", f.Title, f.Url) + } + } +} -- cgit v1.2.3