1717 */
1818package org .owasp .dependencycheck .data .update ;
1919
20+ import org .hamcrest .Matchers ;
21+ import org .jspecify .annotations .NonNull ;
2022import org .junit .jupiter .api .Nested ;
2123import org .junit .jupiter .api .Test ;
24+ import org .mockito .MockedStatic ;
25+ import org .owasp .dependencycheck .data .update .exception .UpdateException ;
26+ import org .owasp .dependencycheck .utils .DownloadFailedException ;
27+ import org .owasp .dependencycheck .utils .Downloader ;
28+ import org .owasp .dependencycheck .utils .Settings ;
2229
2330import java .net .URI ;
31+ import java .time .ZoneOffset ;
32+ import java .time .ZonedDateTime ;
33+ import java .util .List ;
34+ import java .util .Map ;
2435import java .util .NoSuchElementException ;
2536
37+ import static org .hamcrest .MatcherAssert .assertThat ;
38+ import static org .hamcrest .Matchers .contains ;
39+ import static org .hamcrest .Matchers .everyItem ;
2640import static org .junit .jupiter .api .Assertions .assertEquals ;
41+ import static org .junit .jupiter .api .Assertions .assertFalse ;
2742import static org .junit .jupiter .api .Assertions .assertThrows ;
43+ import static org .junit .jupiter .api .Assertions .assertTrue ;
44+ import static org .mockito .ArgumentMatchers .any ;
45+ import static org .mockito .Mockito .mock ;
46+ import static org .mockito .Mockito .mockStatic ;
47+ import static org .mockito .Mockito .when ;
2848import static org .owasp .dependencycheck .data .update .NvdApiDataSource .FeedUrl .DEFAULT_FILE_PATTERN ;
2949import static org .owasp .dependencycheck .data .update .NvdApiDataSource .FeedUrl .extractFromUrlOptionalPattern ;
50+ import static org .owasp .dependencycheck .data .update .NvdApiDataSource .FeedUrl .isMandatoryFeedYear ;
3051
31- /**
32- *
33- * @author Jeremy Long
34- */
3552class NvdApiDataSourceTest {
3653
3754 @ Nested
38- class FeedUrl {
55+ class FeedUrlParsing {
3956
4057 @ Test
4158 void shouldExtractUrlWithPattern () throws Exception {
@@ -47,12 +64,12 @@ void shouldExtractUrlWithPattern() throws Exception {
4764 assertEquals (URI .create (expectedUrl ).toURL (), result .toFormattedUrl ("2045" ));
4865 assertEquals (URI .create ("https://internal.server/nist/some-file.txt" ).toURL (), result .toSuffixedUrl ("some-file.txt" ));
4966
50- assertEquals (expectedUrl , result .toFormattedUrlString (2045 ));
51- assertEquals (URI .create (expectedUrl ).toURL (), result .toFormattedUrl (2045 ));
67+ assertEquals (expectedUrl , result .toFormattedUrlString (" 2045" ));
68+ assertEquals (URI .create (expectedUrl ).toURL (), result .toFormattedUrl (" 2045" ));
5269 }
5370
5471 @ Test
55- void shouldAllowTransformingFilePattern () throws Exception {
72+ void shouldAllowTransformingFilePattern () {
5673 NvdApiDataSource .FeedUrl result = extractFromUrlOptionalPattern ("https://internal.server/nist/nvdcve-{0}.json.gz" )
5774 .withPattern (p -> p .orElseThrow ().replace (".json.gz" , ".something" ));
5875 assertEquals ("https://internal.server/nist/nvdcve-ok.something" , result .toFormattedUrlString ("ok" ));
@@ -80,7 +97,7 @@ void shouldExtractUrlWithoutPattern() throws Exception {
8097 }
8198
8299 @ Test
83- void extractUrlWithoutPatternShouldAddTrailingSlashes () throws Exception {
100+ void extractUrlWithoutPatternShouldAddTrailingSlashes () {
84101 String nvdDataFeedUrl = "https://internal.server/nist" ;
85102 String expectedUrl = "https://internal.server/nist/nvdcve-2045.json.gz" ;
86103
@@ -90,4 +107,115 @@ void extractUrlWithoutPatternShouldAddTrailingSlashes() throws Exception {
90107 assertEquals (expectedUrl , result .toFormattedUrlString ("2045" ));
91108 }
92109 }
110+
111+ @ Nested
112+ class FeedUrlMandatoryYears {
113+
114+ @ Test
115+ void shouldConsiderYearsMandatoryWhenNotCurrentYearAtEarliestTZ () {
116+ ZonedDateTime janFirst2004AtEarliest = ZonedDateTime .of (2004 , 1 , 1 , 0 , 0 , 0 , 0 , NvdApiDataSource .FeedUrl .ZONE_GLOBAL_EARLIEST );
117+ assertTrue (isMandatoryFeedYear (janFirst2004AtEarliest , 2002 ));
118+ assertTrue (isMandatoryFeedYear (janFirst2004AtEarliest , 2003 ));
119+ assertFalse (isMandatoryFeedYear (janFirst2004AtEarliest , 2004 ));
120+ }
121+
122+ @ Test
123+ void shouldConsiderYearsMandatoryWhenNotCurrentYearAtLatestTZ () {
124+ ZonedDateTime janFirst2004AtLatest = ZonedDateTime .of (2004 , 1 , 1 , 0 , 0 , 0 , 0 , NvdApiDataSource .FeedUrl .ZONE_GLOBAL_LATEST );
125+ assertTrue (isMandatoryFeedYear (janFirst2004AtLatest , 2002 ));
126+ assertTrue (isMandatoryFeedYear (janFirst2004AtLatest , 2003 ));
127+ assertFalse (isMandatoryFeedYear (janFirst2004AtLatest , 2004 ));
128+ }
129+
130+ @ Test
131+ void shouldConsiderYearsMandatoryWhenNoLongerJan1Anywhere () {
132+ // It's still Jan 1 somewhere...
133+ ZonedDateTime janSecond2004AtEarliest = ZonedDateTime .of (2004 , 1 , 2 , 0 , 0 , 0 , 0 , NvdApiDataSource .FeedUrl .ZONE_GLOBAL_EARLIEST );
134+ assertFalse (isMandatoryFeedYear (janSecond2004AtEarliest , 2004 ));
135+
136+ // Until it's no longer Jan 1 anywhere
137+ ZonedDateTime janSecond2004AtLatest = ZonedDateTime .of (2004 , 1 , 2 , 0 , 0 , 0 , 1 , NvdApiDataSource .FeedUrl .ZONE_GLOBAL_LATEST );
138+ assertTrue (isMandatoryFeedYear (janSecond2004AtLatest , 2004 ));
139+ }
140+ }
141+
142+ @ Nested
143+ class FeedUrlMetadataRetrieval {
144+
145+ @ Test
146+ void shouldRetrieveMetadataByYear () throws Exception {
147+ try (MockedStatic <Downloader > downloaderClass = mockStatic (Downloader .class )) {
148+ Downloader downloader = mock (Downloader .class );
149+ when (downloader .fetchContent (any (), any ())).thenReturn ("lastModifiedDate=2013-01-01T12:00:00Z" );
150+ downloaderClass .when (Downloader ::getInstance ).thenReturn (downloader );
151+
152+ assertThat (retrieveUntil (ZonedDateTime .of (2003 , 12 , 1 , 0 , 0 , 0 , 0 , ZoneOffset .UTC )).keySet (),
153+ contains ("lastModifiedDate.2002" , "lastModifiedDate.2003" ));
154+ }
155+ }
156+
157+ @ Test
158+ void shouldRetrieveMetadataForNextYearOnJan1AtEarliestTZ () throws Exception {
159+ try (MockedStatic <Downloader > downloaderClass = mockStatic (Downloader .class )) {
160+ Downloader downloader = mock (Downloader .class );
161+ when (downloader .fetchContent (any (), any ())).thenReturn ("lastModifiedDate=2013-01-01T12:00:00Z" );
162+ downloaderClass .when (Downloader ::getInstance ).thenReturn (downloader );
163+
164+ ZonedDateTime jan1Earliest = ZonedDateTime .of (2004 , 1 , 1 , 0 , 0 , 0 , 0 , NvdApiDataSource .FeedUrl .ZONE_GLOBAL_EARLIEST );
165+ assertThat (retrieveUntil (jan1Earliest .minusSeconds (1 )).keySet (),
166+ contains ("lastModifiedDate.2002" , "lastModifiedDate.2003" ));
167+
168+ assertThat (retrieveUntil (jan1Earliest ).keySet (),
169+ contains ("lastModifiedDate.2002" , "lastModifiedDate.2003" , "lastModifiedDate.2004" ));
170+
171+ assertThat (retrieveUntil (ZonedDateTime .of (2004 , 1 , 1 , 0 , 0 , 0 , 0 , NvdApiDataSource .FeedUrl .ZONE_GLOBAL_LATEST )).keySet (),
172+ contains ("lastModifiedDate.2002" , "lastModifiedDate.2003" , "lastModifiedDate.2004" ));
173+ }
174+ }
175+
176+ @ Test
177+ void shouldNormallyRethrowDownloadErrorsEvenIfJan1OnEndYear () throws Exception {
178+ try (MockedStatic <Downloader > downloaderClass = mockStatic (Downloader .class )) {
179+ Downloader downloader = mock (Downloader .class );
180+ when (downloader .fetchContent (any (), any ())).thenThrow (new DownloadFailedException ("failed to download" ));
181+ downloaderClass .when (Downloader ::getInstance ).thenReturn (downloader );
182+
183+ assertThrows (UpdateException .class , () -> retrieveUntil (ZonedDateTime .of (2003 , 1 , 1 , 0 , 0 , 0 , 0 , ZoneOffset .UTC )));
184+ }
185+ }
186+
187+ @ Test
188+ void shouldIgnoreDownloadFailureForFinalYearIfStillJan1 () throws Exception {
189+ List <ZonedDateTime > untilDates = List .of (
190+ ZonedDateTime .of (2004 , 1 , 1 , 0 , 0 , 0 , 0 , NvdApiDataSource .FeedUrl .ZONE_GLOBAL_EARLIEST ),
191+ ZonedDateTime .of (2004 , 1 , 2 , 0 , 0 , 0 , 0 , NvdApiDataSource .FeedUrl .ZONE_GLOBAL_LATEST )
192+ .minusSeconds (1 )
193+ );
194+
195+ for (ZonedDateTime until : untilDates ) {
196+ try (MockedStatic <Downloader > downloaderClass = mockStatic (Downloader .class )) {
197+ Downloader downloader = mock (Downloader .class );
198+ when (downloader .fetchContent (any (), any ()))
199+ .thenReturn ("lastModifiedDate=2013-01-01T12:00:00Z" )
200+ .thenReturn ("lastModifiedDate=2013-01-01T12:00:00Z" )
201+ .thenThrow (new DownloadFailedException ("failed to download 3rd file" ));
202+
203+ downloaderClass .when (Downloader ::getInstance ).thenReturn (downloader );
204+
205+ assertThat (retrieveUntil (until ).keySet (),
206+ contains ("lastModifiedDate.2002" , "lastModifiedDate.2003" ));
207+ }
208+ }
209+ }
210+
211+ private @ NonNull Map <String , ZonedDateTime > retrieveUntil (ZonedDateTime dec12th ) throws UpdateException {
212+ Map <String , ZonedDateTime > lastModifieds ;
213+ NvdApiDataSource .FeedUrl feedUrl = extractFromUrlOptionalPattern ("https://internal.server/nist/nvdcve-{0}.json.gz" );
214+
215+ lastModifieds = feedUrl .getLastModifiedDatePropertiesByYear (new Settings (), dec12th );
216+
217+ assertThat (lastModifieds .values (), everyItem (Matchers .equalTo (ZonedDateTime .of (2013 , 1 , 1 , 12 , 0 , 0 , 0 , ZoneOffset .UTC ))));
218+ return lastModifieds ;
219+ }
220+ }
93221}
0 commit comments