diff --git a/apps/updateserver/services/ExtensionMetadataReader.cfc b/apps/updateserver/services/ExtensionMetadataReader.cfc index 71c36fb..3dc0f28 100644 --- a/apps/updateserver/services/ExtensionMetadataReader.cfc +++ b/apps/updateserver/services/ExtensionMetadataReader.cfc @@ -11,9 +11,9 @@ component accessors=true { variables._simpleCache = StructNew( "max:100" ); } - function loadMeta( srcMeta="" ) { + function loadMeta( query srcMeta ) { lock type="exclusive" name="readExtMeta" timeout=0 { - var meta = len( arguments.srcMeta ) ? arguments.srcMeta : _readExistingMetaFileFromS3(); + var meta = isQuery( arguments.srcMeta ) ? arguments.srcMeta : _readExistingMetaFileFromS3(); var existingByFile = _mapExtensionQueryByFilename( meta ); var lexFiles = _listLexFilesFromBucket(); var metaChanged = false; @@ -75,6 +75,8 @@ component accessors=true { if ( !arguments.all ) { extensions = _stripAllButLatestVersions( extensions ); + // After grouping to latest version, sort by name (and versionSortable desc for tie-breaker) + QuerySort( extensions, "name,versionSortable", "asc,desc" ); } if ( arguments.type != "all" ) { diff --git a/tests/staticArtifacts/testExtensionNameOrder.json b/tests/staticArtifacts/testExtensionNameOrder.json new file mode 100644 index 0000000..ec1eb92 --- /dev/null +++ b/tests/staticArtifacts/testExtensionNameOrder.json @@ -0,0 +1,139 @@ +{ + "COLUMNS": [ + "id", + "version", + "versionSortable", + "name", + "description", + "filename", + "image", + "category", + "author", + "created", + "releaseType", + "minLoaderVersion", + "minCoreVersion", + "price", + "currency", + "disableFull", + "trial", + "older", + "olderName", + "olderDate", + "promotionLevel", + "promotionText", + "projectUrl", + "sourceUrl", + "documentionUrl" + ], + "DATA": [ + [ + "058215B3-5544-4392-A187A1649EB5CA90", + "2.3.0.7-BETA", + "02.003.000.0007.050", + "WebSockets Client Extension", + "Simple client for Websocket interaction for testing.", + "websocket-client-extension-2.3.0.7-BETA.lex", + "", + "web", + "", + "2025-03-10 17:19:30", + "server", + "5.3.0.20", + "", + "", + "", + false, + "", + "", + "", + "", + "", + "", + "", + "", + "" + ], + [ + "07082C66-510A-4F0B-B5E63814E2FDF7BE", + "1.0.0.4-BETA", + "01.000.000.0004.050", + "WebSockets Extension", + "WebSockets server extension.", + "websockets-extension-1.0.0.4-BETA.lex", + "", + "web", + "", + "2024-12-01 10:00:00", + "server", + "5.3.0.20", + "", + "", + "", + "", + false, + "", + "", + "", + "", + "", + "", + "", + "" + ], + [ + "16FF9B13-C595-4FA7-B87DED467B7E61A0", + "4.0.0.5-SNAPSHOT", + "04.000.000.0005.000", + "Memcached Driver", + "Free and open source, high-performance, distributed memory object caching system.", + "memcached-extension-4.0.0.5-SNAPSHOT.lex", + "", + "cache", + "", + "2023-10-10 13:58:00", + "server", + "5.0.0.230", + "", + "", + "", + "", + false, + "", + "", + "", + "", + "", + "", + "", + "" + ], + [ + "17AB52DE-B300-A94B-E058BD978511E39E", + "2.0.2.16-SNAPSHOT", + "02.000.002.0016.000", + "S3 Resource Extension", + "Core Extension to integrate Amazon Simple Storage Service (S3) Resource into Lucee.", + "s3-extension-2.0.2.16-SNAPSHOT.lex", + "", + "resource", + "", + "2023-08-15 18:24:06", + "server", + "5.0.0.157", + "", + "", + "", + "", + false, + "", + "", + "", + "", + "", + "", + "", + "" + ] + ] +} \ No newline at end of file diff --git a/tests/testExtensionNameOrder.cfc b/tests/testExtensionNameOrder.cfc new file mode 100644 index 0000000..eab0248 --- /dev/null +++ b/tests/testExtensionNameOrder.cfc @@ -0,0 +1,122 @@ +component extends="org.lucee.cfml.test.LuceeTestCase" labels="data-provider-integration" { + + function beforeAll(){ + variables.dir = getDirectoryFromPath(getCurrentTemplatePath()); + variables.testVersions = deserializeJson(FileRead("staticArtifacts/testExtensionCoreVersions.json"), false); + + application action="update" mappings={ + "/services" : expandPath( dir & "../apps/updateserver/services" ) + }; + variables.extMetaReader = new services.ExtensionMetadataReader(); + variables.extMetaReader.loadMeta( variables.testVersions ); + + variables.compressExt = "8D7FB0DF-08BB-1589-FE3975678F07DB17"; + variables.imageExt = "B737ABC4-D43F-4D91-8E8E973E37C40D1B"; + } + + function run( testResults , testBox ) { + describe( "LDEV-5699 test extension listing by name", function() { + + it (title="test extension list is sorted by name (production data)", body=function(){ + // Load production-like data from static artifact + var dir = getDirectoryFromPath(getCurrentTemplatePath()); + // Lucee will convert {COLUMNS,DATA} JSON to a query automatically + var qry = deserializeJson(FileRead("staticArtifacts/testExtensionNameOrder.json"), false); + expect(qry).toBeQuery(); + + // Use a fresh reader to ensure no caching effects + var extMetaReader = new services.ExtensionMetadataReader(); + extMetaReader.loadMeta(qry); + var extQuery = extMetaReader.list(); + var names = []; + for (var i=1; i <= extQuery.recordCount; i++) { + arrayAppend(names, extQuery.name[i]); + } + var sortedNames = duplicate(names); + arraySort(sortedNames, "textnocase"); + expect(names).toBe(sortedNames); + }); + + it (title="test extension list is sorted by name (fresh reader)", body=function(){ + var dir = getDirectoryFromPath(getCurrentTemplatePath()); + var testVersions = deserializeJson(FileRead("staticArtifacts/testExtensionCoreVersions.json"), false); + var extMetaReader = new services.ExtensionMetadataReader(); + extMetaReader.loadMeta(testVersions); + var extQuery = extMetaReader.list(); + var names = []; + for (var i=1; i <= extQuery.recordCount; i++) { + arrayAppend(names, extQuery.name[i]); + } + var sortedNames = duplicate(names); + arraySort(sortedNames, "textnocase"); + expect(names).toBe(sortedNames); + }); + + it (title="test extension list is sorted by name (randomized input)", body=function(){ + var dir = getDirectoryFromPath(getCurrentTemplatePath()); + var testVersions = deserializeJson(FileRead("staticArtifacts/testExtensionCoreVersions.json"), false); + // Randomize the order of the query rows + var rows = []; + for (var i=1; i <= testVersions.recordCount; i++) { + arrayAppend(rows, i); + } + arrayShuffle(rows); + var randomized = queryNew(testVersions.columnList); + for (var idx in rows) { + queryAddRow(randomized); + for (var col in listToArray(testVersions.columnList)) { + randomized[col][randomized.recordCount] = testVersions[col][rows[idx]]; + } + } + var extMetaReader = new services.ExtensionMetadataReader(); + extMetaReader.loadMeta(randomized); + var extQuery = extMetaReader.list(); + var names = []; + for (var i=1; i <= extQuery.recordCount; i++) { + arrayAppend(names, extQuery.name[i]); + } + var sortedNames = duplicate(names); + arraySort(sortedNames, "textnocase"); + expect(names).toBe(sortedNames); + }); + + it (title="test extension list is sorted by name (reverse input)", body=function(){ + var dir = getDirectoryFromPath(getCurrentTemplatePath()); + var testVersions = deserializeJson(FileRead("staticArtifacts/testExtensionCoreVersions.json"), false); + // Reverse the order of the query rows + var rows = []; + for (var i=testVersions.recordCount; i >= 1; i--) { + arrayAppend(rows, i); + } + var reversed = queryNew(testVersions.columnList); + for (var idx in rows) { + queryAddRow(reversed); + for (var col in listToArray(testVersions.columnList)) { + reversed[col][reversed.recordCount] = testVersions[col][rows[idx]]; + } + } + var extMetaReader = new services.ExtensionMetadataReader(); + extMetaReader.loadMeta(reversed); + var extQuery = extMetaReader.list(); + var names = []; + for (var i=1; i <= extQuery.recordCount; i++) { + arrayAppend(names, extQuery.name[i]); + } + var sortedNames = duplicate(names); + arraySort(sortedNames, "textnocase"); + expect(names).toBe(sortedNames); + }); + }); + } + + // Fisher-Yates shuffle + private function arrayShuffle(arr) { + for (var i = arrayLen(arr); i > 1; i--) { + var j = randRange(1, i); + var tmp = arr[i]; + arr[i] = arr[j]; + arr[j] = tmp; + } + return arr; + } +}