From 2cfeefc8d27d7fa7716172dadce6de3b9c5bdbe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Guillot?= Date: Mon, 7 Jul 2025 16:56:00 -0700 Subject: [PATCH] test(processor): increase test coverage for `parseISO8601Duration` --- internal/reader/processor/reading_time.go | 2 +- internal/reader/processor/utils.go | 6 +- internal/reader/processor/utils_test.go | 83 +++++++++++++++++++++++ internal/reader/processor/youtube.go | 2 +- internal/reader/processor/youtube_test.go | 30 -------- 5 files changed, 88 insertions(+), 35 deletions(-) diff --git a/internal/reader/processor/reading_time.go b/internal/reader/processor/reading_time.go index 11cb499e..f0f93924 100644 --- a/internal/reader/processor/reading_time.go +++ b/internal/reader/processor/reading_time.go @@ -43,7 +43,7 @@ func fetchWatchTime(websiteURL, query string, isoDate bool) (int, error) { ret := 0 if isoDate { - parsedDuration, err := parseISO8601(duration) + parsedDuration, err := parseISO8601Duration(duration) if err != nil { return 0, fmt.Errorf("unable to parse iso duration %s: %v", duration, err) } diff --git a/internal/reader/processor/utils.go b/internal/reader/processor/utils.go index a57c0f21..2db31cd1 100644 --- a/internal/reader/processor/utils.go +++ b/internal/reader/processor/utils.go @@ -14,9 +14,9 @@ import ( "github.com/tdewolff/minify/v2/html" ) -// parseISO8601 parses a restricted subset of ISO8601 dates, mainly for youtube video durations -func parseISO8601(from string) (time.Duration, error) { - after, ok := strings.CutPrefix(from, "PT") +// parseISO8601Duration parses a subset of ISO8601 durations, mainly for youtube video. +func parseISO8601Duration(duration string) (time.Duration, error) { + after, ok := strings.CutPrefix(duration, "PT") if !ok { return 0, errors.New("the period doesn't start with PT") } diff --git a/internal/reader/processor/utils_test.go b/internal/reader/processor/utils_test.go index 5eac6254..199abe51 100644 --- a/internal/reader/processor/utils_test.go +++ b/internal/reader/processor/utils_test.go @@ -5,8 +5,91 @@ package processor // import "miniflux.app/v2/internal/reader/processor" import ( "testing" + "time" ) +func TestISO8601DurationParsing(t *testing.T) { + var scenarios = []struct { + duration string + expected time.Duration + }{ + // Live streams and radio. + {"PT0M0S", 0}, + // https://www.youtube.com/watch?v=HLrqNhgdiC0 + {"PT6M20S", (6 * time.Minute) + (20 * time.Second)}, + // https://www.youtube.com/watch?v=LZa5KKfqHtA + {"PT5M41S", (5 * time.Minute) + (41 * time.Second)}, + // https://www.youtube.com/watch?v=yIxEEgEuhT4 + {"PT51M52S", (51 * time.Minute) + (52 * time.Second)}, + // https://www.youtube.com/watch?v=bpHf1XcoiFs + {"PT80M42S", (1 * time.Hour) + (20 * time.Minute) + (42 * time.Second)}, + // Hours only + {"PT2H", 2 * time.Hour}, + // Seconds only + {"PT30S", 30 * time.Second}, + // Hours and minutes + {"PT1H30M", (1 * time.Hour) + (30 * time.Minute)}, + // Hours and seconds + {"PT2H45S", (2 * time.Hour) + (45 * time.Second)}, + // Empty duration + {"PT", 0}, + } + + for _, tc := range scenarios { + result, err := parseISO8601Duration(tc.duration) + if err != nil { + t.Errorf("Got an error when parsing %q: %v", tc.duration, err) + } + + if tc.expected != result { + t.Errorf(`Unexpected result, got %v for duration %q`, result, tc.duration) + } + } +} + +func TestISO8601DurationParsingErrors(t *testing.T) { + var errorScenarios = []struct { + duration string + expectedErr string + }{ + // Missing PT prefix + {"6M20S", "the period doesn't start with PT"}, + // Unsupported Year specifier + {"PT1Y", "the 'Y' specifier isn't supported"}, + // Unsupported Week specifier + {"PT2W", "the 'W' specifier isn't supported"}, + // Unsupported Day specifier + {"PT3D", "the 'D' specifier isn't supported"}, + // Invalid number for hours (letter at start of number) + {"PTaH", "invalid character in the period"}, + // Invalid number for minutes (letter at start of number) + {"PTbM", "invalid character in the period"}, + // Invalid number for seconds (letter at start of number) + {"PTcS", "invalid character in the period"}, + // Invalid character in the middle of a number + {"PT1a2H", "invalid character in the period"}, + {"PT3b4M", "invalid character in the period"}, + {"PT5c6S", "invalid character in the period"}, + // Test cases for actual ParseFloat errors (empty number before specifier) + {"PTH", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + {"PTM", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + {"PTS", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + // Invalid character + {"PT1X", "invalid character in the period"}, + // Invalid character mixed + {"PT1H@M", "invalid character in the period"}, + } + + for _, tc := range errorScenarios { + _, err := parseISO8601Duration(tc.duration) + if err == nil { + t.Errorf("Expected an error when parsing %q, but got none", tc.duration) + } else if err.Error() != tc.expectedErr { + t.Errorf("Expected error %q when parsing %q, but got %q", tc.expectedErr, tc.duration, err.Error()) + } + } +} + func TestMinifyEntryContentWithWhitespace(t *testing.T) { input := `

Some text with a link

` expected := `

Some text with a link

` diff --git a/internal/reader/processor/youtube.go b/internal/reader/processor/youtube.go index ee696499..68aeec62 100644 --- a/internal/reader/processor/youtube.go +++ b/internal/reader/processor/youtube.go @@ -118,7 +118,7 @@ func fetchYouTubeWatchTimeFromApiInBulk(videoIDs []string) (map[string]time.Dura watchTimeMap := make(map[string]time.Duration, len(videos.Items)) for _, video := range videos.Items { - duration, err := parseISO8601(video.ContentDetails.Duration) + duration, err := parseISO8601Duration(video.ContentDetails.Duration) if err != nil { slog.Warn("Unable to parse ISO8601 duration", slog.Any("error", err)) continue diff --git a/internal/reader/processor/youtube_test.go b/internal/reader/processor/youtube_test.go index 9018abcd..dac660cd 100644 --- a/internal/reader/processor/youtube_test.go +++ b/internal/reader/processor/youtube_test.go @@ -5,38 +5,8 @@ package processor // import "miniflux.app/v2/internal/reader/processor" import ( "testing" - "time" ) -func TestParseISO8601(t *testing.T) { - var scenarios = []struct { - duration string - expected time.Duration - }{ - // Live streams and radio. - {"PT0M0S", 0}, - // https://www.youtube.com/watch?v=HLrqNhgdiC0 - {"PT6M20S", (6 * time.Minute) + (20 * time.Second)}, - // https://www.youtube.com/watch?v=LZa5KKfqHtA - {"PT5M41S", (5 * time.Minute) + (41 * time.Second)}, - // https://www.youtube.com/watch?v=yIxEEgEuhT4 - {"PT51M52S", (51 * time.Minute) + (52 * time.Second)}, - // https://www.youtube.com/watch?v=bpHf1XcoiFs - {"PT80M42S", (1 * time.Hour) + (20 * time.Minute) + (42 * time.Second)}, - } - - for _, tc := range scenarios { - result, err := parseISO8601(tc.duration) - if err != nil { - t.Errorf("Got an error when parsing %q: %v", tc.duration, err) - } - - if tc.expected != result { - t.Errorf(`Unexpected result, got %v for duration %q`, result, tc.duration) - } - } -} - func TestGetYouTubeVideoIDFromURL(t *testing.T) { scenarios := []struct { url string