From ac15e23739968cc8b9de8a9133e3b071f0638dc8 Mon Sep 17 00:00:00 2001
From: Chris van der Pennen <chrisvanderpennen@users.noreply.github.com>
Date: Sun, 15 Oct 2023 18:42:02 +1030
Subject: [PATCH] fix(nuget): Sort api response before picking projectUrl
 (#23090)

Co-authored-by: Rhys Arkins <rhys@arkins.net>
Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
---
 .../azure_devops/nunit/nuspec.xml             | 31 ++++++++++
 .../azure_devops/nunit/v3_registration.json   | 56 +++++++++++++++++++
 .../__fixtures__/azure_devops/v3_index.json   | 14 +++++
 lib/modules/datasource/nuget/index.spec.ts    | 54 ++++++++++++++++++
 lib/modules/datasource/nuget/v3.spec.ts       | 19 +++++++
 lib/modules/datasource/nuget/v3.ts            | 27 ++++++++-
 6 files changed, 199 insertions(+), 2 deletions(-)
 create mode 100644 lib/modules/datasource/nuget/__fixtures__/azure_devops/nunit/nuspec.xml
 create mode 100644 lib/modules/datasource/nuget/__fixtures__/azure_devops/nunit/v3_registration.json
 create mode 100644 lib/modules/datasource/nuget/__fixtures__/azure_devops/v3_index.json
 create mode 100644 lib/modules/datasource/nuget/v3.spec.ts

diff --git a/lib/modules/datasource/nuget/__fixtures__/azure_devops/nunit/nuspec.xml b/lib/modules/datasource/nuget/__fixtures__/azure_devops/nunit/nuspec.xml
new file mode 100644
index 0000000000..03af655703
--- /dev/null
+++ b/lib/modules/datasource/nuget/__fixtures__/azure_devops/nunit/nuspec.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
+  <metadata minClientVersion="2.12">
+    <id>NUnit</id>
+    <version>3.13.2</version>
+    <title>NUnit</title>
+    <authors>Charlie Poole, Rob Prouse</authors>
+    <owners>Charlie Poole, Rob Prouse</owners>
+    <requireLicenseAcceptance>false</requireLicenseAcceptance>
+    <license type="file">LICENSE.txt</license>
+    <licenseUrl>https://aka.ms/deprecateLicenseUrl</licenseUrl>
+    <icon>icon.png</icon>
+    <projectUrl>https://nunit.org/</projectUrl>
+    <iconUrl>https://cdn.rawgit.com/nunit/resources/master/images/icon/nunit_256.png</iconUrl>
+    <description></description>
+    <summary>NUnit is a unit-testing framework for all .NET languages with a strong TDD focus.</summary>
+    <releaseNotes></releaseNotes>
+    <copyright>Copyright (c) 2021 Charlie Poole, Rob Prouse</copyright>
+    <language>en-US</language>
+    <tags>nunit test testing tdd framework fluent assert theory plugin addin</tags>
+    <repository type="git" url="https://github.com/nunit/nunit" />
+    <dependencies>
+      <group targetFramework=".NETFramework3.5" />
+      <group targetFramework=".NETFramework4.0" />
+      <group targetFramework=".NETFramework4.5" />
+      <group targetFramework=".NETStandard2.0">
+        <dependency id="NETStandard.Library" version="2.0.0" />
+      </group>
+    </dependencies>
+  </metadata>
+</package>
diff --git a/lib/modules/datasource/nuget/__fixtures__/azure_devops/nunit/v3_registration.json b/lib/modules/datasource/nuget/__fixtures__/azure_devops/nunit/v3_registration.json
new file mode 100644
index 0000000000..4499672162
--- /dev/null
+++ b/lib/modules/datasource/nuget/__fixtures__/azure_devops/nunit/v3_registration.json
@@ -0,0 +1,56 @@
+{
+  "count": 1,
+  "items": [
+    {
+      "count": 4,
+      "items": [
+        {
+          "@id": "https://pkgs.dev.azure.com/organisationName/_packaging/2745c5e9-610a-4537-9032-978c66527b51/nuget/v3/registrations2/nunit/3.13.2.json",
+          "@type": "Package",
+          "catalogEntry": {
+            "listed": true,
+            "projectUrl": "https://nunit.org/",
+            "published": "2021-12-03T03:20:52Z",
+            "version": "3.13.2"
+          },
+          "packageContent": "https://pkgs.dev.azure.com/organisationName/_packaging/2745c5e9-610a-4537-9032-978c66527b51/nuget/v3/flat2/nunit/3.13.2/nunit.3.13.2.nupkg"
+        },
+        {
+          "@id": "https://pkgs.dev.azure.com/organisationName/_packaging/2745c5e9-610a-4537-9032-978c66527b51/nuget/v3/registrations2/nunit/2.7.1.json",
+          "@type": "Package",
+          "catalogEntry": {
+            "listed": true,
+            "projectUrl": "http://nunitsoftware.com/nunitv2",
+            "published": "2021-12-03T03:20:52Z",
+            "version": "2.7.1"
+          },
+          "packageContent": "https://pkgs.dev.azure.com/organisationName/_packaging/2745c5e9-610a-4537-9032-978c66527b51/nuget/v3/flat2/nunit/2.7.1/nunit.2.7.1.nupkg"
+        },
+        {
+          "@id": "https://pkgs.dev.azure.com/organisationName/_packaging/2745c5e9-610a-4537-9032-978c66527b51/nuget/v3/registrations2/nunit/2.6.5.json",
+          "@type": "Package",
+          "catalogEntry": {
+            "listed": true,
+            "projectUrl": "http://nunit.org/",
+            "published": "2021-12-03T03:20:52Z",
+            "version": "2.6.5"
+          },
+          "packageContent": "https://pkgs.dev.azure.com/organisationName/_packaging/2745c5e9-610a-4537-9032-978c66527b51/nuget/v3/flat2/nunit/2.6.5/nunit.2.6.5.nupkg"
+        },
+        {
+          "@id": "https://pkgs.dev.azure.com/organisationName/_packaging/2745c5e9-610a-4537-9032-978c66527b51/nuget/v3/registrations2/nunit/2.5.7.10213.json",
+          "@type": "Package",
+          "catalogEntry": {
+            "listed": true,
+            "projectUrl": "",
+            "published": "2021-12-03T03:20:52Z",
+            "version": "2.5.7.10213"
+          },
+          "packageContent": "https://pkgs.dev.azure.com/organisationName/_packaging/2745c5e9-610a-4537-9032-978c66527b51/nuget/v3/flat2/nunit/2.5.7.10213/nunit.2.5.7.10213.nupkg"
+        }
+      ],
+      "lower": "2.5.7.10213",
+      "upper": "3.13.2"
+    }
+  ]
+}
diff --git a/lib/modules/datasource/nuget/__fixtures__/azure_devops/v3_index.json b/lib/modules/datasource/nuget/__fixtures__/azure_devops/v3_index.json
new file mode 100644
index 0000000000..af67f8fa18
--- /dev/null
+++ b/lib/modules/datasource/nuget/__fixtures__/azure_devops/v3_index.json
@@ -0,0 +1,14 @@
+{
+  "resources": [
+    {
+      "@id": "https://pkgs.dev.azure.com/organisationName/_packaging/2745c5e9-610a-4537-9032-978c66527b51/nuget/v3/registrations2-semver2/",
+      "@type": "RegistrationsBaseUrl/3.6.0",
+      "comment": "This base URL includes SemVer 2.0.0 packages."
+    },
+    {
+      "@id": "https://pkgs.dev.azure.com/organisationName/_packaging/2745c5e9-610a-4537-9032-978c66527b51/nuget/v3/flat2/",
+      "@type": "PackageBaseAddress/3.0.0"
+    }
+  ],
+  "version": "3.0.0-beta"
+}
diff --git a/lib/modules/datasource/nuget/index.spec.ts b/lib/modules/datasource/nuget/index.spec.ts
index b0b56b29ec..816bbd16bb 100644
--- a/lib/modules/datasource/nuget/index.spec.ts
+++ b/lib/modules/datasource/nuget/index.spec.ts
@@ -94,6 +94,15 @@ const configV3Multiple = {
   ],
 };
 
+const configV3AzureDevOps = {
+  datasource,
+  versioning,
+  packageName: 'nunit',
+  registryUrls: [
+    'https://pkgs.dev.azure.com/organisationName/_packaging/2745c5e9-610a-4537-9032-978c66527b51/nuget/v3/index.json',
+  ],
+};
+
 describe('modules/datasource/nuget/index', () => {
   describe('parseRegistryUrl', () => {
     it('extracts feed version from registry URL hash (v3)', () => {
@@ -374,6 +383,51 @@ describe('modules/datasource/nuget/index', () => {
       expect(res?.sourceUrl).toBeDefined();
     });
 
+    it('processes real data (v3) feed is azure devops', async () => {
+      httpMock
+        .scope('https://pkgs.dev.azure.com')
+        .get(
+          '/organisationName/_packaging/2745c5e9-610a-4537-9032-978c66527b51/nuget/v3/index.json'
+        )
+        .twice()
+        .reply(200, Fixtures.get('azure_devops/v3_index.json'))
+        .get(
+          '/organisationName/_packaging/2745c5e9-610a-4537-9032-978c66527b51/nuget/v3/registrations2-semver2/nunit/index.json'
+        )
+        .reply(200, Fixtures.get('azure_devops/nunit/v3_registration.json'))
+        .get(
+          '/organisationName/_packaging/2745c5e9-610a-4537-9032-978c66527b51/nuget/v3/flat2/nunit/3.13.2/nunit.nuspec'
+        )
+        .reply(200, Fixtures.get('azure_devops/nunit/nuspec.xml'));
+      const res = await getPkgReleases({
+        ...configV3AzureDevOps,
+      });
+      expect(res).toMatchObject({
+        homepage: 'https://nunit.org/',
+        registryUrl:
+          'https://pkgs.dev.azure.com/organisationName/_packaging/2745c5e9-610a-4537-9032-978c66527b51/nuget/v3/index.json',
+        releases: [
+          {
+            releaseTimestamp: '2021-12-03T03:20:52.000Z',
+            version: '2.5.7.10213',
+          },
+          {
+            releaseTimestamp: '2021-12-03T03:20:52.000Z',
+            version: '2.6.5',
+          },
+          {
+            releaseTimestamp: '2021-12-03T03:20:52.000Z',
+            version: '2.7.1',
+          },
+          {
+            releaseTimestamp: '2021-12-03T03:20:52.000Z',
+            version: '3.13.2',
+          },
+        ],
+        sourceUrl: 'https://github.com/nunit/nunit',
+      });
+    });
+
     it('processes real data (v3) for several catalog pages', async () => {
       const scope = httpMock
         .scope('https://api.nuget.org')
diff --git a/lib/modules/datasource/nuget/v3.spec.ts b/lib/modules/datasource/nuget/v3.spec.ts
new file mode 100644
index 0000000000..61ae82b983
--- /dev/null
+++ b/lib/modules/datasource/nuget/v3.spec.ts
@@ -0,0 +1,19 @@
+import { sortNugetVersions } from './v3';
+
+describe('modules/datasource/nuget/v3', () => {
+  it.each<{ version: string; other: string; result: number }>`
+    version         | other           | result
+    ${'invalid1'}   | ${'invalid2'}   | ${0}
+    ${'invalid'}    | ${'1.0.0'}      | ${-1}
+    ${'1.0.0'}      | ${'invalid'}    | ${1}
+    ${'1.0.0-rc.1'} | ${'1.0.0'}      | ${-1}
+    ${'1.0.0'}      | ${'1.0.0-rc.1'} | ${1}
+    ${'1.0.0'}      | ${'1.0.0'}      | ${0}
+  `(
+    'sortNugetVersions("$version", "$other") === $result',
+    ({ version, other, result }) => {
+      const res = sortNugetVersions(version, other);
+      expect(res).toBe(result);
+    }
+  );
+});
diff --git a/lib/modules/datasource/nuget/v3.ts b/lib/modules/datasource/nuget/v3.ts
index 2c760a9f44..eb5e088c11 100644
--- a/lib/modules/datasource/nuget/v3.ts
+++ b/lib/modules/datasource/nuget/v3.ts
@@ -8,6 +8,7 @@ import { Http, HttpError } from '../../../util/http';
 import * as p from '../../../util/promises';
 import { regEx } from '../../../util/regex';
 import { ensureTrailingSlash } from '../../../util/url';
+import { api as versioning } from '../../versioning/nuget';
 import type { Release, ReleaseResult } from '../types';
 import { massageUrl, removeBuildMeta } from './common';
 import type {
@@ -110,6 +111,26 @@ async function getCatalogEntry(
   return items.map(({ catalogEntry }) => catalogEntry);
 }
 
+/**
+ * Compare two versions. Return:
+ * - `1` if `a > b` or `b` is invalid
+ * - `-1` if `a < b` or `a` is invalid
+ * - `0` if `a == b` or both `a` and `b` are invalid
+ */
+export function sortNugetVersions(a: string, b: string): number {
+  if (versioning.isValid(a)) {
+    if (versioning.isValid(b)) {
+      return versioning.sortVersions(a, b);
+    } else {
+      return 1;
+    }
+  } else if (versioning.isValid(b)) {
+    return -1;
+  } else {
+    return 0;
+  }
+}
+
 export async function getReleases(
   http: Http,
   registryUrl: string,
@@ -123,7 +144,9 @@ export async function getReleases(
   const catalogPagesQueue = catalogPages.map(
     (page) => (): Promise<CatalogEntry[]> => getCatalogEntry(http, page)
   );
-  const catalogEntries = (await p.all(catalogPagesQueue)).flat();
+  const catalogEntries = (await p.all(catalogPagesQueue))
+    .flat()
+    .sort((a, b) => sortNugetVersions(a.version, b.version));
 
   let homepage: string | null = null;
   let latestStable: string | null = null;
@@ -133,7 +156,7 @@ export async function getReleases(
       if (releaseTimestamp) {
         release.releaseTimestamp = releaseTimestamp;
       }
-      if (semver.valid(version) && !semver.prerelease(version)) {
+      if (versioning.isValid(version) && versioning.isStable(version)) {
         latestStable = removeBuildMeta(version);
         homepage = projectUrl ? massageUrl(projectUrl) : homepage;
       }
-- 
GitLab