Skip to content

Commit 8ac0bcb

Browse files
author
Scott Bommarito
authored
If ConfirmEmailAddresses config is false, users should not be required to confirm changing their email (#6314)
1 parent 1b3707e commit 8ac0bcb

5 files changed

Lines changed: 140 additions & 26 deletions

File tree

src/NuGetGallery.Core/Entities/User.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ public void CancelChangeEmailAddress()
154154
UnconfirmedEmailAddress = null;
155155
}
156156

157-
public void UpdateEmailAddress(string newEmailAddress, Func<string> generateToken)
157+
public void UpdateUnconfirmedEmailAddress(string newEmailAddress, Func<string> generateToken)
158158
{
159159
if (!string.IsNullOrEmpty(UnconfirmedEmailAddress))
160160
{

src/NuGetGallery/Controllers/AccountsController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ public virtual async Task<ActionResult> ChangeEmail(TAccountViewModel model)
252252
return AccountView(account, model);
253253
}
254254

255-
if (account.Confirmed)
255+
if (account.Confirmed && !string.IsNullOrEmpty(account.UnconfirmedEmailAddress))
256256
{
257257
SendEmailChangedConfirmationNotice(account);
258258
}

src/NuGetGallery/Services/UserService.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,13 @@ public async Task ChangeEmailAddress(User user, string newEmailAddress)
386386

387387
await Auditing.SaveAuditRecordAsync(new UserAuditRecord(user, AuditedUserAction.ChangeEmail, newEmailAddress));
388388

389-
user.UpdateEmailAddress(newEmailAddress, Crypto.GenerateToken);
389+
user.UpdateUnconfirmedEmailAddress(newEmailAddress, Crypto.GenerateToken);
390+
391+
if (!Config.ConfirmEmailAddresses)
392+
{
393+
user.ConfirmEmailAddress();
394+
}
395+
390396
await UserRepository.CommitChangesAsync();
391397
}
392398

@@ -555,6 +561,11 @@ public async Task<Organization> AddOrganizationAsync(string username, string ema
555561
Members = new List<Membership>()
556562
};
557563

564+
if (!Config.ConfirmEmailAddresses)
565+
{
566+
organization.ConfirmEmailAddress();
567+
}
568+
558569
var membership = new Membership { Organization = organization, Member = adminUser, IsAdmin = true };
559570

560571
organization.Members.Add(membership);

tests/NuGetGallery.Facts/Controllers/AccountsControllerFacts.cs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,23 @@ public virtual async Task WhenNewEmailIsSame_RedirectsWithoutChange(Func<Fakes,
227227

228228
[Theory]
229229
[MemberData(AllowedCurrentUsersDataName)]
230-
public virtual async Task WhenNewEmailIsDifferentAndWasConfirmed_SavesChanges(Func<Fakes, User> getCurrentUser)
230+
public virtual Task WhenNewEmailIsUnconfirmedAndDifferentAndWasConfirmed_SavesChanges(Func<Fakes, User> getCurrentUser)
231+
{
232+
return WhenNewEmailIsDifferentAndWasConfirmedHelper(getCurrentUser, newEmailIsConfirmed: false);
233+
}
234+
235+
[Theory]
236+
[MemberData(AllowedCurrentUsersDataName)]
237+
public virtual Task WhenNewEmailIsConfirmedAndDifferentAndWasConfirmed_SavesChanges(Func<Fakes, User> getCurrentUser)
238+
{
239+
return WhenNewEmailIsDifferentAndWasConfirmedHelper(getCurrentUser, newEmailIsConfirmed: true);
240+
}
241+
242+
/// <remarks>
243+
/// Normally, you should use a single <see cref="TheoryAttribute"/> that enumerates through the possible values of <paramref name="getCurrentUser"/> and <paramref name="newEmailIsConfirmed"/>,
244+
/// but because we are using test case "inheritance" (search for properties with the same name as <see cref="AllowedCurrentUsersDataName"/>), this is not possible.
245+
/// </remarks>
246+
private async Task WhenNewEmailIsDifferentAndWasConfirmedHelper(Func<Fakes, User> getCurrentUser, bool newEmailIsConfirmed)
231247
{
232248
// Arrange
233249
var controller = GetController();
@@ -236,15 +252,15 @@ public virtual async Task WhenNewEmailIsDifferentAndWasConfirmed_SavesChanges(Fu
236252
model.ChangeEmail.NewEmail = "[email protected]";
237253

238254
// Act
239-
var result = await InvokeChangeEmail(controller, account, getCurrentUser, model);
255+
var result = await InvokeChangeEmail(controller, account, getCurrentUser, model, newEmailIsConfirmed);
240256

241257
// Assert
242258
GetMock<IUserService>().Verify(u => u.ChangeEmailAddress(It.IsAny<User>(), It.IsAny<string>()), Times.Once);
243259
ResultAssert.IsRedirectToRoute(result, new { action = controller.AccountAction });
244260

245261
GetMock<IMessageService>()
246262
.Verify(m => m.SendEmailChangeConfirmationNotice(It.IsAny<User>(), It.IsAny<string>()),
247-
Times.Once);
263+
newEmailIsConfirmed ? Times.Never() : Times.Once());
248264
}
249265

250266
[Theory]
@@ -285,6 +301,7 @@ protected virtual Task<ActionResult> InvokeChangeEmail(
285301
TUser account,
286302
Func<Fakes, User> getCurrentUser,
287303
TAccountViewModel model = null,
304+
bool newEmailIsConfirmed = false,
288305
EntityException exception = null)
289306
{
290307
// Arrange
@@ -299,7 +316,17 @@ protected virtual Task<ActionResult> InvokeChangeEmail(
299316
.Returns(account as User);
300317

301318
var setup = userService.Setup(u => u.ChangeEmailAddress(It.IsAny<User>(), It.IsAny<string>()))
302-
.Callback<User, string>((acct, newEmail) => { acct.UnconfirmedEmailAddress = newEmail; });
319+
.Callback<User, string>((acct, newEmail) =>
320+
{
321+
if (newEmailIsConfirmed)
322+
{
323+
acct.EmailAddress = newEmail;
324+
}
325+
else
326+
{
327+
acct.UnconfirmedEmailAddress = newEmail;
328+
}
329+
});
303330

304331
if (exception != null)
305332
{

tests/NuGetGallery.Facts/Services/UserServiceFacts.cs

Lines changed: 95 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1156,13 +1156,38 @@ public async Task SetsUnconfirmedEmailWhenEmailIsChanged()
11561156
Users = new[] { user }
11571157
};
11581158

1159+
service.MockConfig
1160+
.Setup(x => x.ConfirmEmailAddresses)
1161+
.Returns(true);
1162+
11591163
await service.ChangeEmailAddress(user, "[email protected]");
11601164

11611165
Assert.Equal("[email protected]", user.EmailAddress);
11621166
Assert.Equal("[email protected]", user.UnconfirmedEmailAddress);
11631167
service.FakeEntitiesContext.VerifyCommitChanges();
11641168
}
11651169

1170+
[Fact]
1171+
public async Task AutomaticallyConfirmsWhenConfirmEmailAddressesConfigurationIsFalse()
1172+
{
1173+
var user = new User { Username = "Bob", EmailAddress = "[email protected]" };
1174+
var service = new TestableUserServiceWithDBFaking
1175+
{
1176+
Users = new[] { user }
1177+
};
1178+
1179+
service.MockConfig
1180+
.Setup(x => x.ConfirmEmailAddresses)
1181+
.Returns(false);
1182+
1183+
await service.ChangeEmailAddress(user, "[email protected]");
1184+
1185+
Assert.Equal("[email protected]", user.EmailAddress);
1186+
Assert.Null(user.UnconfirmedEmailAddress);
1187+
Assert.Null(user.EmailConfirmationToken);
1188+
service.FakeEntitiesContext.VerifyCommitChanges();
1189+
}
1190+
11661191
/// <summary>
11671192
/// It has to change the pending confirmation token whenever address changes because otherwise you can do
11681193
/// 1. change address, get confirmation email
@@ -1178,6 +1203,10 @@ public async Task ModifiesConfirmationTokenWhenEmailAddressChanged()
11781203
Users = new User[] { user },
11791204
};
11801205

1206+
service.MockConfig
1207+
.Setup(x => x.ConfirmEmailAddresses)
1208+
.Returns(true);
1209+
11811210
await service.ChangeEmailAddress(user, "[email protected]");
11821211
Assert.NotNull(user.EmailConfirmationToken);
11831212
Assert.NotEmpty(user.EmailConfirmationToken);
@@ -1197,6 +1226,10 @@ public async Task DoesNotModifyAnythingWhenConfirmedEmailAddressNotChanged()
11971226
Users = new User[] { user },
11981227
};
11991228

1229+
service.MockConfig
1230+
.Setup(x => x.ConfirmEmailAddresses)
1231+
.Returns(true);
1232+
12001233
await service.ChangeEmailAddress(user, "[email protected]");
12011234
Assert.True(user.Confirmed);
12021235
Assert.Equal("[email protected]", user.EmailAddress);
@@ -1221,6 +1254,10 @@ public async Task DoesNotModifyConfirmationTokenWhenUnconfirmedEmailAddressNotCh
12211254
Users = new User[] { user },
12221255
};
12231256

1257+
service.MockConfig
1258+
.Setup(x => x.ConfirmEmailAddresses)
1259+
.Returns(true);
1260+
12241261
await service.ChangeEmailAddress(user, "[email protected]");
12251262
Assert.Equal("pending-token", user.EmailConfirmationToken);
12261263
}
@@ -1784,11 +1821,16 @@ public class TheAddOrganizationAccountMethod
17841821

17851822
private TestableUserService _service = new TestableUserService();
17861823

1787-
[Fact]
1788-
public async Task WithUsernameConflict_ThrowsEntityException()
1824+
public static IEnumerable<object[]> ConfirmEmailAddresses_Config => MemberDataHelper.AsDataSet(false, true);
1825+
1826+
[Theory]
1827+
[MemberData(nameof(ConfirmEmailAddresses_Config))]
1828+
public async Task WithUsernameConflict_ThrowsEntityException(bool confirmEmailAddresses)
17891829
{
17901830
var conflictUsername = "ialreadyexist";
17911831

1832+
SetUpConfirmEmailAddressesConfig(confirmEmailAddresses);
1833+
17921834
_service.MockEntitiesContext
17931835
.Setup(x => x.Users)
17941836
.Returns(new[] { new User(conflictUsername) }.MockDbSet().Object);
@@ -1802,11 +1844,14 @@ public async Task WithUsernameConflict_ThrowsEntityException()
18021844
Assert.False(_service.Auditing.WroteRecord<UserAuditRecord>());
18031845
}
18041846

1805-
[Fact]
1806-
public async Task WithEmailConflict_ThrowsEntityException()
1847+
[Theory]
1848+
[MemberData(nameof(ConfirmEmailAddresses_Config))]
1849+
public async Task WithEmailConflict_ThrowsEntityException(bool confirmEmailAddresses)
18071850
{
18081851
var conflictEmail = "[email protected]";
18091852

1853+
SetUpConfirmEmailAddressesConfig(confirmEmailAddresses);
1854+
18101855
_service.MockEntitiesContext
18111856
.Setup(x => x.Users)
18121857
.Returns(new[] { new User("user") { EmailAddress = conflictEmail } }.MockDbSet().Object);
@@ -1820,25 +1865,31 @@ public async Task WithEmailConflict_ThrowsEntityException()
18201865
Assert.False(_service.Auditing.WroteRecord<UserAuditRecord>());
18211866
}
18221867

1823-
[Fact]
1824-
public async Task WhenAdminHasNoTenant_ReturnsNewOrgWithoutPolicy()
1868+
[Theory]
1869+
[MemberData(nameof(ConfirmEmailAddresses_Config))]
1870+
public async Task WhenAdminHasNoTenant_ReturnsNewOrgWithoutPolicy(bool confirmEmailAddresses)
18251871
{
18261872
_service.MockEntitiesContext
18271873
.Setup(x => x.Users)
18281874
.Returns(Enumerable.Empty<User>().MockDbSet().Object);
18291875

1876+
SetUpConfirmEmailAddressesConfig(confirmEmailAddresses);
1877+
18301878
var org = await InvokeAddOrganization(admin: new User(AdminName) { Credentials = new Credential[0] });
18311879

1832-
AssertNewOrganizationReturned(org, subscribedToPolicy: false);
1880+
AssertNewOrganizationReturned(org, subscribedToPolicy: false, confirmEmailAddresses: confirmEmailAddresses);
18331881
}
18341882

1835-
[Fact]
1836-
public async Task WhenAdminHasUnsupportedTenant_ReturnsNewOrgWithoutPolicy()
1883+
[Theory]
1884+
[MemberData(nameof(ConfirmEmailAddresses_Config))]
1885+
public async Task WhenAdminHasUnsupportedTenant_ReturnsNewOrgWithoutPolicy(bool confirmEmailAddresses)
18371886
{
18381887
_service.MockEntitiesContext
18391888
.Setup(x => x.Users)
18401889
.Returns(Enumerable.Empty<User>().MockDbSet().Object);
18411890

1891+
SetUpConfirmEmailAddressesConfig(confirmEmailAddresses);
1892+
18421893
var mockLoginDiscontinuationConfiguration = new Mock<ILoginDiscontinuationConfiguration>();
18431894
mockLoginDiscontinuationConfiguration
18441895
.Setup(x => x.IsTenantIdPolicySupportedForOrganization(It.IsAny<string>(), It.IsAny<string>()))
@@ -1850,16 +1901,19 @@ public async Task WhenAdminHasUnsupportedTenant_ReturnsNewOrgWithoutPolicy()
18501901

18511902
var org = await InvokeAddOrganization();
18521903

1853-
AssertNewOrganizationReturned(org, subscribedToPolicy: false);
1904+
AssertNewOrganizationReturned(org, subscribedToPolicy: false, confirmEmailAddresses: confirmEmailAddresses);
18541905
}
18551906

1856-
[Fact]
1857-
public async Task WhenSubscribingToPolicyFails_ReturnsNewOrgWithoutPolicy()
1907+
[Theory]
1908+
[MemberData(nameof(ConfirmEmailAddresses_Config))]
1909+
public async Task WhenSubscribingToPolicyFails_ReturnsNewOrgWithoutPolicy(bool confirmEmailAddresses)
18581910
{
18591911
_service.MockEntitiesContext
18601912
.Setup(x => x.Users)
18611913
.Returns(Enumerable.Empty<User>().MockDbSet().Object);
18621914

1915+
SetUpConfirmEmailAddressesConfig(confirmEmailAddresses);
1916+
18631917
var mockLoginDiscontinuationConfiguration = new Mock<ILoginDiscontinuationConfiguration>();
18641918
mockLoginDiscontinuationConfiguration
18651919
.Setup(x => x.IsTenantIdPolicySupportedForOrganization(It.IsAny<string>(), It.IsAny<string>()))
@@ -1875,16 +1929,19 @@ public async Task WhenSubscribingToPolicyFails_ReturnsNewOrgWithoutPolicy()
18751929

18761930
var org = await InvokeAddOrganization();
18771931

1878-
AssertNewOrganizationReturned(org, subscribedToPolicy: true);
1932+
AssertNewOrganizationReturned(org, subscribedToPolicy: true, confirmEmailAddresses: confirmEmailAddresses);
18791933
}
18801934

1881-
[Fact]
1882-
public async Task WhenSubscribingToPolicySucceeds_ReturnsNewOrg()
1935+
[Theory]
1936+
[MemberData(nameof(ConfirmEmailAddresses_Config))]
1937+
public async Task WhenSubscribingToPolicySucceeds_ReturnsNewOrg(bool confirmEmailAddresses)
18831938
{
18841939
_service.MockEntitiesContext
18851940
.Setup(x => x.Users)
18861941
.Returns(Enumerable.Empty<User>().MockDbSet().Object);
18871942

1943+
SetUpConfirmEmailAddressesConfig(confirmEmailAddresses);
1944+
18881945
var mockLoginDiscontinuationConfiguration = new Mock<ILoginDiscontinuationConfiguration>();
18891946
mockLoginDiscontinuationConfiguration
18901947
.Setup(x => x.IsTenantIdPolicySupportedForOrganization(It.IsAny<string>(), It.IsAny<string>()))
@@ -1900,7 +1957,7 @@ public async Task WhenSubscribingToPolicySucceeds_ReturnsNewOrg()
19001957

19011958
var org = await InvokeAddOrganization();
19021959

1903-
AssertNewOrganizationReturned(org, subscribedToPolicy: true);
1960+
AssertNewOrganizationReturned(org, subscribedToPolicy: true, confirmEmailAddresses: confirmEmailAddresses);
19041961
}
19051962

19061963
private Task<Organization> InvokeAddOrganization(string orgName = OrgName, string orgEmail = OrgEmail, User admin = null)
@@ -1925,14 +1982,26 @@ private Task<Organization> InvokeAddOrganization(string orgName = OrgName, strin
19251982
return _service.AddOrganizationAsync(orgName, orgEmail, admin);
19261983
}
19271984

1928-
private void AssertNewOrganizationReturned(Organization org, bool subscribedToPolicy)
1985+
private void AssertNewOrganizationReturned(Organization org, bool subscribedToPolicy, bool confirmEmailAddresses)
19291986
{
19301987
Assert.Equal(OrgName, org.Username);
1931-
Assert.Equal(OrgEmail, org.UnconfirmedEmailAddress);
1988+
1989+
if (confirmEmailAddresses)
1990+
{
1991+
Assert.Null(org.EmailAddress);
1992+
Assert.Equal(OrgEmail, org.UnconfirmedEmailAddress);
1993+
Assert.NotNull(org.EmailConfirmationToken);
1994+
}
1995+
else
1996+
{
1997+
Assert.Null(org.UnconfirmedEmailAddress);
1998+
Assert.Equal(OrgEmail, org.EmailAddress);
1999+
Assert.Null(org.EmailConfirmationToken);
2000+
}
2001+
19322002
Assert.Equal(OrgCreatedUtc, org.CreatedUtc);
19332003
Assert.True(org.EmailAllowed);
19342004
Assert.True(org.NotifyPackagePushed);
1935-
Assert.True(!string.IsNullOrEmpty(org.EmailConfirmationToken));
19362005

19372006
// Both the organization and the admin must have a membership to each other.
19382007
Func<Membership, bool> hasMembership = m => m.Member.Username == AdminName && m.Organization.Username == OrgName && m.IsAdmin;
@@ -1949,6 +2018,13 @@ private void AssertNewOrganizationReturned(Organization org, bool subscribedToPo
19492018
ar.AffectedMemberIsAdmin == true));
19502019
_service.MockEntitiesContext.Verify(x => x.SaveChangesAsync(), Times.Once());
19512020
}
2021+
2022+
private void SetUpConfirmEmailAddressesConfig(bool confirmEmailAddresses)
2023+
{
2024+
_service.MockConfig
2025+
.Setup(x => x.ConfirmEmailAddresses)
2026+
.Returns(confirmEmailAddresses);
2027+
}
19522028
}
19532029
public class TheRejectTransformUserToOrganizationRequestMethod
19542030
{

0 commit comments

Comments
 (0)