diff --git a/tests/NuGetGallery.FunctionalTests.Core/Helpers/ClientSdkHelper.cs b/tests/NuGetGallery.FunctionalTests.Core/Helpers/ClientSdkHelper.cs index daa50cec6d..086b11bc56 100644 --- a/tests/NuGetGallery.FunctionalTests.Core/Helpers/ClientSdkHelper.cs +++ b/tests/NuGetGallery.FunctionalTests.Core/Helpers/ClientSdkHelper.cs @@ -89,7 +89,7 @@ public bool CheckIfPackageVersionExistsInSource(string packageId, string version WriteLine("[verification attempt {0}]: Checking if package {1} with version {2} exists in source {3}... ", i, packageId, version, sourceUrl); IPackage package = repo.FindPackage(packageId, semVersion); - found = (package != null); + found = package != null; if (found) { WriteLine("Found!"); @@ -110,10 +110,19 @@ public bool CheckIfPackageVersionExistsInSource(string packageId, string version } /// - /// Creates a package with the specified Id and Version and uploads it and checks if the upload has suceeded. - /// This will be used by test classes which tests scenarios on top of upload. + /// Creates a package with the specified Id and Version and uploads it and checks if the upload has succeeded. + /// Throws if the upload fails or cannot be verified in the source. /// public async Task UploadNewPackageAndVerify(string packageId, string version = "1.0.0", string minClientVersion = null, string title = null, string tags = null, string description = null, string licenseUrl = null, string dependencies = null) + { + await UploadNewPackage(packageId, version, minClientVersion, title, tags, description, licenseUrl, dependencies); + + VerifyPackageExistsInSource(packageId, version); + } + + public async Task UploadNewPackage(string packageId, string version = "1.0.0", string minClientVersion = null, + string title = null, string tags = null, string description = null, string licenseUrl = null, + string dependencies = null) { if (string.IsNullOrEmpty(packageId)) { @@ -128,13 +137,11 @@ public async Task UploadNewPackageAndVerify(string packageId, string version = " var commandlineHelper = new CommandlineHelper(TestOutputHelper); var processResult = await commandlineHelper.UploadPackageAsync(packageFullPath, UrlHelper.V2FeedPushSourceUrl); - Assert.True(processResult.ExitCode == 0, "The package upload via Nuget.exe did not succeed properly. Check the logs to see the process error and output stream. Exit Code: " + processResult.ExitCode + ". Error message: \"" + processResult.StandardError + "\""); + Assert.True(processResult.ExitCode == 0, + "The package upload via Nuget.exe did not succeed properly. Check the logs to see the process error and output stream. Exit Code: " + + processResult.ExitCode + ". Error message: \"" + processResult.StandardError + "\""); - var packageExistsInSource = CheckIfPackageVersionExistsInSource(packageId, version, UrlHelper.V2FeedRootUrl); - var userMessage = string.Format("Package {0} with version {1} is not found in the site {2} after uploading.", packageId, version, UrlHelper.V2FeedRootUrl); - Assert.True(packageExistsInSource, userMessage); - - // Delete package from local disk so once it gets uploaded + // Delete package from local disk once it gets uploaded if (File.Exists(packageFullPath)) { File.Delete(packageFullPath); @@ -142,6 +149,46 @@ public async Task UploadNewPackageAndVerify(string packageId, string version = " } } + /// + /// Unlists a package with the specified Id and Version and checks if the unlist has succeeded. + /// Throws if the unlist fails or cannot be verified in the source. + /// + public async Task UnlistPackageAndVerify(string packageId, string version = "1.0.0") + { + await UnlistPackage(packageId, version); + + VerifyPackageExistsInSource(packageId, version); + } + + public async Task UnlistPackage(string packageId, string version = "1.0.0") + { + if (string.IsNullOrEmpty(packageId)) + { + throw new ArgumentException($"{nameof(packageId)} cannot be null or empty!"); + } + + WriteLine("Unlisting package '{0}', version '{1}'", packageId, version); + + var commandlineHelper = new CommandlineHelper(TestOutputHelper); + var processResult = await commandlineHelper.DeletePackageAsync(packageId, version, UrlHelper.V2FeedPushSourceUrl); + + Assert.True(processResult.ExitCode == 0, + "The package unlist via Nuget.exe did not succeed properly. Check the logs to see the process error and output stream. Exit Code: " + + processResult.ExitCode + ". Error message: \"" + processResult.StandardError + "\""); + } + + /// + /// Throws if the specified package cannot be found in the source. + /// + /// Id of the package. + /// Version of the package. + public void VerifyPackageExistsInSource(string packageId, string version = "1.0.0") + { + var packageExistsInSource = CheckIfPackageVersionExistsInSource(packageId, version, UrlHelper.V2FeedRootUrl); + Assert.True(packageExistsInSource, + $"Package {packageId} with version {version} is not found on the site {UrlHelper.V2FeedRootUrl}."); + } + /// /// Returns the latest stable version string for the given package. /// diff --git a/tests/NuGetGallery.FunctionalTests.Core/Helpers/CommandlineHelper.cs b/tests/NuGetGallery.FunctionalTests.Core/Helpers/CommandlineHelper.cs index c4008c26fe..80f56ad215 100644 --- a/tests/NuGetGallery.FunctionalTests.Core/Helpers/CommandlineHelper.cs +++ b/tests/NuGetGallery.FunctionalTests.Core/Helpers/CommandlineHelper.cs @@ -20,6 +20,7 @@ public class CommandlineHelper internal static string PackCommandString = " pack "; internal static string UpdateCommandString = " update "; internal static string InstallCommandString = " install "; + internal static string DeleteCommandString = " delete "; internal static string PushCommandString = " push "; internal static string OutputDirectorySwitchString = " -OutputDirectory "; internal static string PreReleaseSwitchString = " -Prerelease "; @@ -51,6 +52,19 @@ public async Task UploadPackageAsync(string packageFullPath, stri return await InvokeNugetProcess(arguments); } + /// + /// Delete the specified package using Nuget.exe + /// + /// package to be deleted + /// version of package to be deleted + /// source url + /// + public async Task DeletePackageAsync(string packageId, string version, string sourceName) + { + var arguments = string.Join(string.Empty, DeleteCommandString, packageId, " ", version, SourceSwitchString, sourceName, ApiKeySwitchString, EnvironmentSettings.TestAccountApiKey); + return await InvokeNugetProcess(arguments); + } + /// /// Install the specified package using Nuget.exe /// diff --git a/tests/NuGetGallery.FunctionalTests.Core/Helpers/ODataHelper.cs b/tests/NuGetGallery.FunctionalTests.Core/Helpers/ODataHelper.cs index c8b1f4ba12..08c948a43b 100644 --- a/tests/NuGetGallery.FunctionalTests.Core/Helpers/ODataHelper.cs +++ b/tests/NuGetGallery.FunctionalTests.Core/Helpers/ODataHelper.cs @@ -59,17 +59,61 @@ public async Task TryDownloadPackageFromFeed(string packageId, string ve } } - public async Task ContainsResponseText(string url, params string[] expectedTexts) + public async Task GetTimestampOfPackageFromResponse(string url, string propertyName, string packageId, string version = "1.0.0") { - var request = WebRequest.Create(url); - var response = await request.GetResponseAsync().ConfigureAwait(false); + WriteLine($"Getting '{propertyName}' timestamp of package '{packageId}' with version '{version}'."); - string responseText; - using (var sr = new StreamReader(response.GetResponseStream())) + var packageResponse = await GetPackageDataInResponse(url, packageId, version); + if (string.IsNullOrEmpty(packageResponse)) + { + return null; + } + + var timestampStartTag = ""; + var timestampEndTag = ""; + + var timestampTagIndex = packageResponse.IndexOf(timestampStartTag); + if (timestampTagIndex < 0) { - responseText = await sr.ReadToEndAsync().ConfigureAwait(false); + WriteLine($"Package data does not contain '{propertyName}' timestamp!"); + return null; } + var timestampStartIndex = timestampTagIndex + timestampStartTag.Length; + var timestampLength = packageResponse.Substring(timestampStartIndex).IndexOf(timestampEndTag); + + var timestamp = + DateTime.Parse(packageResponse.Substring(timestampStartIndex, timestampLength)); + WriteLine($"'{propertyName}' timestamp of package '{packageId}' with version '{version}' is '{timestamp}'"); + return timestamp; + } + + public async Task GetPackageDataInResponse(string url, string packageId, string version = "1.0.0") + { + WriteLine($"Getting data for package '{packageId}' with version '{version}'."); + + var responseText = await GetResponseText(url); + + var packageString = @"" + UrlHelper.V2FeedRootUrl + @"Packages(Id='" + packageId + @"',Version='" + (string.IsNullOrEmpty(version) ? "" : version + "')"); + var endEntryTag = ""; + + var startingIndex = responseText.IndexOf(packageString); + + if (startingIndex < 0) + { + WriteLine("Package not found in response text!"); + return null; + } + + var endingIndex = responseText.IndexOf(endEntryTag, startingIndex); + + return responseText.Substring(startingIndex, endingIndex - startingIndex); + } + + public async Task ContainsResponseText(string url, params string[] expectedTexts) + { + var responseText = await GetResponseText(url); + foreach (string s in expectedTexts) { if (!responseText.Contains(s)) @@ -83,14 +127,7 @@ public async Task ContainsResponseText(string url, params string[] expecte public async Task ContainsResponseTextIgnoreCase(string url, params string[] expectedTexts) { - var request = WebRequest.Create(url); - var response = await request.GetResponseAsync(); - - string responseText; - using (var sr = new StreamReader(response.GetResponseStream())) - { - responseText = (await sr.ReadToEndAsync()).ToLowerInvariant(); - } + var responseText = (await GetResponseText(url)).ToLowerInvariant(); foreach (string s in expectedTexts) { @@ -103,15 +140,29 @@ public async Task ContainsResponseTextIgnoreCase(string url, params string return true; } + private async Task GetResponseText(string url) + { + var request = WebRequest.Create(url); + var response = await request.GetResponseAsync(); + + string responseText; + using (var sr = new StreamReader(response.GetResponseStream())) + { + responseText = await sr.ReadToEndAsync(); + } + + return responseText; + } + public async Task DownloadPackageFromV2FeedWithOperation(string packageId, string version, string operation) { string filename = await DownloadPackageFromFeed(packageId, version, operation); - //check if the file exists. + // Check if the file exists. Assert.True(File.Exists(filename), Constants.PackageDownloadFailureMessage); var clientSdkHelper = new ClientSdkHelper(TestOutputHelper); string downloadedPackageId = clientSdkHelper.GetPackageIdFromNupkgFile(filename); - //Check that the downloaded Nupkg file is not corrupt and it indeed corresponds to the package which we were trying to download. + // Check that the downloaded Nupkg file is not corrupt and it indeed corresponds to the package which we were trying to download. Assert.True(downloadedPackageId.Equals(packageId), Constants.UnableToZipError); } diff --git a/tests/NuGetGallery.FunctionalTests/ODataFeeds/V2FeedExtendedTests.cs b/tests/NuGetGallery.FunctionalTests/ODataFeeds/V2FeedExtendedTests.cs index f502d7dd0d..6765feff9c 100644 --- a/tests/NuGetGallery.FunctionalTests/ODataFeeds/V2FeedExtendedTests.cs +++ b/tests/NuGetGallery.FunctionalTests/ODataFeeds/V2FeedExtendedTests.cs @@ -2,9 +2,11 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Net; +using System.Threading; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; @@ -28,21 +30,27 @@ public V2FeedExtendedTests(ITestOutputHelper testOutputHelper) _packageCreationHelper = new PackageCreationHelper(TestOutputHelper); } + private bool CanUploadToSite() + { + // Temporary workaround for the SSL issue, which keeps the upload test from working with cloudapp.net sites + return UrlHelper.BaseUrl.Contains("nugettest.org") || UrlHelper.BaseUrl.Contains("nuget.org") || + UrlHelper.BaseUrl.Contains("nuget.localtest.me"); + } + [Fact] [Description("Upload two packages and then issue the FindPackagesById request, expect to return both versions")] [Priority(1)] [Category("P0Tests")] public async Task FindPackagesByIdTest() { - // Temporary workaround for the SSL issue, which keeps the upload test from working with cloudapp.net sites - if (UrlHelper.BaseUrl.Contains("nugettest.org") || UrlHelper.BaseUrl.Contains("nuget.org")) + if (CanUploadToSite()) { string packageId = string.Format("TestV2FeedFindPackagesById.{0}", DateTime.UtcNow.Ticks); TestOutputHelper.WriteLine("Uploading package '{0}'", packageId); await _clientSdkHelper.UploadNewPackageAndVerify(packageId); - TestOutputHelper.WriteLine("Uploaded package '{0}'", packageId); + TestOutputHelper.WriteLine("Uploaded package '{0}'", packageId); await _clientSdkHelper.UploadNewPackageAndVerify(packageId, "2.0.0"); string url = UrlHelper.V2FeedRootUrl + @"/FindPackagesById()?id='" + packageId + "'"; @@ -56,6 +64,84 @@ public async Task FindPackagesByIdTest() } } + private const int PackagesInOrderNumPackages = 10; + + [Fact] + [Description("Upload multiple packages and then unlist them and verify that they appear in the feed in the correct order")] + [Priority(1)] + [Category("P0Tests")] + public async Task PackagesAppearInFeedInOrderTest() + { + // This test uploads/unlists packages in a particular order to test the timestamps of the packages in the feed. + // Because it waits for previous requests to finish before starting new ones, it will only catch ordering issues if these issues are greater than a second or two. + // This is consistent with the time frame in which we've seen these issues in the past, but if new issues arise that are on a smaller scale, this test will not catch it! + + if (CanUploadToSite()) + { + var packageIds = new List(PackagesInOrderNumPackages); + var startingTime = DateTime.UtcNow; + + // Upload the packages in order. + var uploadStartTimestamp = DateTime.UtcNow.AddMinutes(-1); + for (var i = 0; i < PackagesInOrderNumPackages; i++) + { + var packageId = GetPackagesAppearInFeedInOrderPackageId(startingTime, i); + await _clientSdkHelper.UploadNewPackage(packageId); + packageIds.Add(packageId); + } + + await CheckPackageTimestampsInOrder(packageIds, "Created", uploadStartTimestamp); + + // Unlist the packages in order. + var unlistStartTimestamp = DateTime.UtcNow.AddMinutes(-1); + for (var i = 0; i < PackagesInOrderNumPackages; i++) + { + await _clientSdkHelper.UnlistPackage(packageIds[i]); + } + + await CheckPackageTimestampsInOrder(packageIds, "LastEdited", unlistStartTimestamp); + } + } + + private static string GetPackagesAppearInFeedInOrderPackageId(DateTime startingTime, int i) + { + return $"TestV2FeedPackagesAppearInFeedInOrderTest.{startingTime.Ticks}.{i}"; + } + + private static string GetPackagesAppearInFeedInOrderUrl(DateTime time, string timestamp) + { + return $"{UrlHelper.V2FeedRootUrl}/Packages?$filter={timestamp} gt DateTime'{time:o}'&$orderby={timestamp} desc&$select={timestamp}"; + } + + /// + /// Verifies if a set of packages in the feed have timestamps in a particular order. + /// + /// An ordered list of package ids. Each package id in the list must have a timestamp in the feed earlier than all package ids after it. + /// The timestamp property to test the ordering of. For example, "Created" or "LastEdited". + /// A timestamp that is before all of the timestamps expected to be found in the feed. This is used in a request to the feed. + private async Task CheckPackageTimestampsInOrder(List packageIds, string timestampPropertyName, + DateTime operationStartTimestamp) + { + var lastTimestamp = DateTime.MinValue; + for (var i = 0; i < PackagesInOrderNumPackages; i++) + { + var packageId = packageIds[i]; + TestOutputHelper.WriteLine($"Attempting to check order of package #{i} {timestampPropertyName} timestamp in feed."); + + var newTimestamp = + await + _odataHelper.GetTimestampOfPackageFromResponse( + GetPackagesAppearInFeedInOrderUrl(operationStartTimestamp, timestampPropertyName), + timestampPropertyName, + packageId); + + Assert.True(newTimestamp.HasValue); + Assert.True(newTimestamp.Value > lastTimestamp, + $"Package #{i} was last modified after package #{i - 1} but has an earlier {timestampPropertyName} timestamp ({newTimestamp} should be greater than {lastTimestamp})."); + lastTimestamp = newTimestamp.Value; + } + } + /// /// Regression test for #1199, also covers #1052 ///