Skip to content

Commit 2365944

Browse files
committed
Add configuration value to disable specific curated feeds (#6486)
Address #6470
1 parent 0646af6 commit 2365944

8 files changed

Lines changed: 179 additions & 56 deletions

File tree

src/NuGetGallery/Configuration/AppConfiguration.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,5 +344,9 @@ public string ExternalBrandingMessage
344344
public bool RejectPackagesWithTooManyPackageEntries { get; set; }
345345

346346
public bool BlockSearchEngineIndexing { get; set; }
347+
348+
[DefaultValue(null)]
349+
[TypeConverter(typeof(StringArrayConverter))]
350+
public string[] DisabledCuratedFeeds { get; set; }
347351
}
348352
}

src/NuGetGallery/Configuration/IAppConfiguration.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,5 +349,11 @@ public interface IAppConfiguration : ICoreMessageServiceConfiguration
349349
/// Whether or not to block search engines from indexing the web pages using the "noindex" meta tag.
350350
/// </summary>
351351
bool BlockSearchEngineIndexing { get; set; }
352+
353+
/// <summary>
354+
/// The name of zero or more curated feeds that are disabled. If a curated feed is disabled, it appears as if
355+
/// it doesn't exist.
356+
/// </summary>
357+
string[] DisabledCuratedFeeds { get; set; }
352358
}
353359
}

src/NuGetGallery/Controllers/ODataV2CuratedFeedController.cs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,16 @@ public class ODataV2CuratedFeedController
2222
{
2323
private const int MaxPageSize = 40;
2424

25-
private readonly IEntitiesContext _entities;
2625
private readonly IGalleryConfigurationService _configurationService;
2726
private readonly ISearchService _searchService;
2827
private readonly ICuratedFeedService _curatedFeedService;
2928

3029
public ODataV2CuratedFeedController(
31-
IEntitiesContext entities,
3230
IGalleryConfigurationService configurationService,
3331
ISearchService searchService,
3432
ICuratedFeedService curatedFeedService)
3533
: base(configurationService)
3634
{
37-
_entities = entities;
3835
_configurationService = configurationService;
3936
_searchService = searchService;
4037
_curatedFeedService = curatedFeedService;
@@ -49,7 +46,7 @@ public IHttpActionResult Get(
4946
string curatedFeedName,
5047
[FromUri] string semVerLevel = null)
5148
{
52-
if (!_entities.CuratedFeeds.Any(cf => cf.Name == curatedFeedName))
49+
if (_curatedFeedService.GetFeedByName(curatedFeedName) == null)
5350
{
5451
return NotFound();
5552
}
@@ -133,7 +130,7 @@ private async Task<IHttpActionResult> GetCore(
133130
bool return404NotFoundWhenNoResults,
134131
string semVerLevel)
135132
{
136-
var curatedFeed = _entities.CuratedFeeds.FirstOrDefault(cf => cf.Name == curatedFeedName);
133+
var curatedFeed = _curatedFeedService.GetFeedByName(curatedFeedName);
137134
if (curatedFeed == null)
138135
{
139136
return NotFound();
@@ -231,7 +228,7 @@ public async Task<IHttpActionResult> Search(
231228
[FromODataUri]bool includePrerelease = false,
232229
[FromUri]string semVerLevel = null)
233230
{
234-
if (!_entities.CuratedFeeds.Any(cf => cf.Name == curatedFeedName))
231+
if (_curatedFeedService.GetFeedByName(curatedFeedName) == null)
235232
{
236233
return NotFound();
237234
}
@@ -254,7 +251,7 @@ public async Task<IHttpActionResult> Search(
254251
}
255252

256253
// Perform actual search
257-
var curatedFeed = _curatedFeedService.GetFeedByName(curatedFeedName, includePackages: false);
254+
var curatedFeed = _curatedFeedService.GetFeedByName(curatedFeedName);
258255
var packages = _curatedFeedService.GetPackages(curatedFeedName)
259256
.Where(p => p.PackageStatusKey == PackageStatus.Available)
260257
.Where(SemVerLevelKey.IsPackageCompliantWithSemVerLevelPredicate(semVerLevel))
Lines changed: 22 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,65 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4-
using System.Collections.Generic;
5-
using System.Data.Entity;
4+
using System;
65
using System.Linq;
6+
using NuGetGallery.Configuration;
77

88
namespace NuGetGallery
99
{
1010
public class CuratedFeedService : ICuratedFeedService
1111
{
1212
protected IEntityRepository<CuratedFeed> CuratedFeedRepository { get; set; }
13-
protected IEntityRepository<CuratedPackage> CuratedPackageRepository { get; set; }
13+
protected IAppConfiguration AppConfiguration { get; set; }
1414

1515
protected CuratedFeedService()
1616
{
1717
}
1818

1919
public CuratedFeedService(
20-
IEntityRepository<CuratedFeed> curatedFeedRepository)
20+
IEntityRepository<CuratedFeed> curatedFeedRepository,
21+
IAppConfiguration appConfiguration)
2122
{
2223
CuratedFeedRepository = curatedFeedRepository;
24+
AppConfiguration = appConfiguration;
2325
}
2426

25-
public CuratedFeed GetFeedByName(string name, bool includePackages)
27+
public CuratedFeed GetFeedByName(string name)
2628
{
27-
IQueryable<CuratedFeed> query = CuratedFeedRepository.GetAll();
28-
29-
if (includePackages)
29+
if (IsCuratedFeedDisabled(name))
3030
{
31-
query = query
32-
.Include(cf => cf.Packages)
33-
.Include(cf => cf.Packages.Select(cp => cp.PackageRegistration));
31+
return null;
3432
}
3533

36-
return query
34+
return CuratedFeedRepository
35+
.GetAll()
3736
.SingleOrDefault(cf => cf.Name == name);
3837
}
3938

40-
public CuratedFeed GetFeedByKey(int key, bool includePackages)
39+
public IQueryable<Package> GetPackages(string curatedFeedName)
4140
{
42-
IQueryable<CuratedFeed> query = CuratedFeedRepository.GetAll();
43-
44-
if (includePackages)
41+
if (IsCuratedFeedDisabled(curatedFeedName))
4542
{
46-
query = query
47-
.Include(cf => cf.Packages)
48-
.Include(cf => cf.Packages.Select(cp => cp.PackageRegistration));
43+
return Enumerable.Empty<Package>().AsQueryable();
4944
}
5045

51-
return query
52-
.SingleOrDefault(cf => cf.Key == key);
53-
}
54-
55-
public IQueryable<Package> GetPackages(string curatedFeedName)
56-
{
5746
var packages = CuratedFeedRepository.GetAll()
5847
.Where(cf => cf.Name == curatedFeedName)
5948
.SelectMany(cf => cf.Packages.SelectMany(cp => cp.PackageRegistration.Packages));
6049

6150
return packages;
6251
}
6352

64-
public IQueryable<PackageRegistration> GetPackageRegistrations(string curatedFeedName)
53+
private bool IsCuratedFeedDisabled(string name)
6554
{
66-
var packageRegistrations = CuratedFeedRepository.GetAll()
67-
.Where(cf => cf.Name == curatedFeedName)
68-
.SelectMany(cf => cf.Packages.Select(cp => cp.PackageRegistration));
69-
70-
return packageRegistrations;
71-
}
72-
73-
public int? GetKey(string curatedFeedName)
74-
{
75-
var results = CuratedFeedRepository.GetAll()
76-
.Where(cf => cf.Name == curatedFeedName)
77-
.Select(cf => cf.Key).Take(1).ToArray();
55+
if (AppConfiguration.DisabledCuratedFeeds == null)
56+
{
57+
return false;
58+
}
7859

79-
return results.Length > 0 ? (int?)results[0] : null;
60+
return AppConfiguration
61+
.DisabledCuratedFeeds
62+
.Contains(name, StringComparer.OrdinalIgnoreCase);
8063
}
8164
}
8265
}

src/NuGetGallery/Services/ICuratedFeedService.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@ namespace NuGetGallery
77
{
88
public interface ICuratedFeedService
99
{
10-
CuratedFeed GetFeedByName(string name, bool includePackages);
11-
CuratedFeed GetFeedByKey(int key, bool includePackages);
10+
CuratedFeed GetFeedByName(string name);
1211
IQueryable<Package> GetPackages(string curatedFeedName);
13-
IQueryable<PackageRegistration> GetPackageRegistrations(string curatedFeedName);
14-
int? GetKey(string curatedFeedName);
1512
}
1613
}

tests/NuGetGallery.Facts/Controllers/ODataV2CuratedFeedControllerFacts.cs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -330,14 +330,9 @@ protected override ODataV2CuratedFeedController CreateController(
330330

331331
var curatedFeedServiceMock = new Mock<ICuratedFeedService>(MockBehavior.Strict);
332332
curatedFeedServiceMock.Setup(m => m.GetPackages(_curatedFeedName)).Returns(AllPackages);
333-
curatedFeedServiceMock.Setup(m => m.GetFeedByName(_curatedFeedName, false)).Returns(curatedFeed);
334-
335-
var entitiesContextMock = new Mock<IEntitiesContext>(MockBehavior.Strict);
336-
var curatedFeedDbSet = GetQueryableMockDbSet(curatedFeed);
337-
entitiesContextMock.SetupGet(m => m.CuratedFeeds).Returns(curatedFeedDbSet);
333+
curatedFeedServiceMock.Setup(m => m.GetFeedByName(_curatedFeedName)).Returns(curatedFeed);
338334

339335
return new ODataV2CuratedFeedController(
340-
entitiesContextMock.Object,
341336
configurationService,
342337
searchService,
343338
curatedFeedServiceMock.Object);

tests/NuGetGallery.Facts/NuGetGallery.Facts.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@
127127
<Compile Include="Security\RequirePackageMetadataCompliancePolicyFacts.cs" />
128128
<Compile Include="Services\ActionRequiringEntityPermissionsFacts.cs" />
129129
<Compile Include="Services\CertificatesConfigurationFacts.cs" />
130+
<Compile Include="Services\CuratedFeedServiceFacts.cs" />
130131
<Compile Include="Services\TyposquattingServiceFacts.cs" />
131132
<Compile Include="Services\CloudDownloadCountServiceFacts.cs" />
132133
<Compile Include="Services\CertificateServiceFacts.cs" />
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Linq;
5+
using Moq;
6+
using NuGetGallery.Configuration;
7+
using Xunit;
8+
9+
namespace NuGetGallery
10+
{
11+
public class CuratedFeedServiceFacts
12+
{
13+
public class TheGetFeedByNameMethod : Facts
14+
{
15+
[Fact]
16+
public void ReturnsNullWhenFeedIsDisabled()
17+
{
18+
DisableFeed(_curatedFeed.Name);
19+
20+
var output = _target.GetFeedByName(_curatedFeed.Name);
21+
22+
Assert.Null(output);
23+
_entityRepository.Verify(x => x.GetAll(), Times.Never);
24+
}
25+
26+
[Fact]
27+
public void ReturnsNullWhenFeedIsDisabledWithDifferentCasing()
28+
{
29+
_curatedFeed.Name = "curated-feed";
30+
DisableFeed("CURATED-Feed");
31+
32+
var output = _target.GetFeedByName(_curatedFeed.Name);
33+
34+
Assert.Null(output);
35+
_entityRepository.Verify(x => x.GetAll(), Times.Never);
36+
}
37+
38+
[Fact]
39+
public void ReturnsFeedWhenNameMatchesAndFeedIsNotDisabled()
40+
{
41+
var output = _target.GetFeedByName(_curatedFeed.Name);
42+
43+
Assert.Same(_curatedFeed, output);
44+
_entityRepository.Verify(x => x.GetAll(), Times.Once);
45+
46+
}
47+
48+
[Fact]
49+
public void ReturnsNullWhenFeedDoesNotExist()
50+
{
51+
var output = _target.GetFeedByName("something-else");
52+
53+
Assert.Null(output);
54+
_entityRepository.Verify(x => x.GetAll(), Times.Once);
55+
}
56+
}
57+
58+
public class TheGetPackagesMethod : Facts
59+
{
60+
[Fact]
61+
public void ReturnsEmptyListWhenFeedIsDisabled()
62+
{
63+
DisableFeed(_curatedFeed.Name);
64+
65+
var output = _target.GetPackages(_curatedFeed.Name);
66+
67+
Assert.Empty(output);
68+
_entityRepository.Verify(x => x.GetAll(), Times.Never);
69+
}
70+
71+
[Fact]
72+
public void ReturnsPackagesWhenNameMatchesAndFeedIsNotDisabled()
73+
{
74+
var output = _target.GetPackages(_curatedFeed.Name);
75+
76+
var package = Assert.Single(output);
77+
Assert.Same(_package, package);
78+
79+
}
80+
81+
[Fact]
82+
public void ReturnsNullWhenFeedDoesNotExist()
83+
{
84+
var output = _target.GetPackages("something-else");
85+
86+
Assert.Empty(output);
87+
_entityRepository.Verify(x => x.GetAll(), Times.Once);
88+
}
89+
}
90+
91+
public abstract class Facts
92+
{
93+
protected Package _package;
94+
protected CuratedFeed _curatedFeed;
95+
protected readonly Mock<IEntityRepository<CuratedFeed>> _entityRepository;
96+
protected readonly Mock<IAppConfiguration> _config;
97+
protected readonly CuratedFeedService _target;
98+
99+
protected Facts()
100+
{
101+
_package = new Package();
102+
_curatedFeed = new CuratedFeed
103+
{
104+
Name = "curated-feed",
105+
Packages = new[]
106+
{
107+
new CuratedPackage
108+
{
109+
PackageRegistration = new PackageRegistration
110+
{
111+
Packages = new[]
112+
{
113+
_package,
114+
},
115+
},
116+
},
117+
},
118+
};
119+
120+
_entityRepository = new Mock<IEntityRepository<CuratedFeed>>();
121+
_config = new Mock<IAppConfiguration>();
122+
123+
_entityRepository
124+
.Setup(x => x.GetAll())
125+
.Returns(() => new[] { _curatedFeed }.AsQueryable());
126+
127+
_target = new CuratedFeedService(
128+
_entityRepository.Object,
129+
_config.Object);
130+
}
131+
132+
protected void DisableFeed(string feedName)
133+
{
134+
_config
135+
.Setup(x => x.DisabledCuratedFeeds)
136+
.Returns(new[] { feedName });
137+
}
138+
}
139+
}
140+
}

0 commit comments

Comments
 (0)