Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions clients/dashboard/src/api/identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,9 +425,9 @@ export async function getMyProfileWithETag(): Promise<{ profile: UserDto; etag:
}

/**
* Updates the authenticated user's profile. Maps to UpdateUserCommand server-side. Image and
* email changes go through their own dedicated endpoints — this is for the editable profile
* fields surfaced in settings/profile. The unedited fields come off `input.profile`, the copy
* Updates the authenticated user's profile. Maps to UpdateUserCommand server-side. The image
* goes through its own endpoint, and the e-mail address can't be changed here (it needs a
* confirmation flow) — this is for the editable profile fields surfaced in settings/profile. The unedited fields come off `input.profile`, the copy
* the form was seeded from, so they keep their values instead of being nulled.
*
* The PUT carries `If-Match` with that same copy's ETag, so the server answers 412 when the
Expand All @@ -447,7 +447,6 @@ export async function updateMyProfile(input: UpdateProfileInput): Promise<void>
firstName: input.firstName ?? profile.firstName ?? null,
lastName: input.lastName ?? profile.lastName ?? null,
phoneNumber: input.phoneNumber ?? profile.phoneNumber ?? null,
email: profile.email,
deleteCurrentImage: false,
}),
});
Expand Down
3 changes: 2 additions & 1 deletion clients/dashboard/tests/settings/profile.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,9 @@ test.describe("settings/profile — wired to PUT /identity/profile", () => {
firstName: "Alicia",
lastName: "Nguyen-Ortiz",
phoneNumber: "+1 (555) 000-1234",
email: TEST_USER.email,
});
// The profile PUT can't change the e-mail address, so the body must not pretend it can.
expect(body).not.toHaveProperty("email");
});

test("shows a success toast on 200", async ({ page }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ public class UpdateUserCommand : ICommand<Unit>
public string? FirstName { get; set; }
public string? LastName { get; set; }
public string? PhoneNumber { get; set; }
public string? Email { get; set; }
public FileUploadRequest? Image { get; set; }
public bool DeleteCurrentImage { get; set; }
public string? Locale { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,6 @@ public UpdateUserCommandValidator(IStringLocalizer<SharedResources> localizer)
.MaximumLength(15)
.When(x => !string.IsNullOrWhiteSpace(x.PhoneNumber));

RuleFor(x => x.Email)
.EmailAddress()
.When(x => !string.IsNullOrWhiteSpace(x.Email));

When(x => x.Image is not null, () =>
{
RuleFor(x => x.Image!)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,24 +71,6 @@ public void Validate_Should_Fail_When_FirstNameExceedsMaxLength()
result.Errors.ShouldContain(e => e.PropertyName == "FirstName");
}

[Fact]
public void Validate_Should_Fail_When_EmailIsInvalid()
{
// Arrange
var command = new UpdateUserCommand
{
Id = "user-123",
Email = "not-an-email"
};

// Act
var result = _sut.Validate(command);

// Assert
result.IsValid.ShouldBeFalse();
result.Errors.ShouldContain(e => e.PropertyName == "Email");
}

[Fact]
public void Validate_Should_Fail_When_DeleteImageAndUploadImage_Simultaneously()
{
Expand Down Expand Up @@ -149,13 +131,13 @@ public void Builtin_validation_message_is_localized_under_ptBR()
{
// Act — FluentValidation resolves built-in messages via CurrentUICulture (ships a pt catalog).
var result = WithCulture("pt-BR", () =>
_sut.Validate(new UpdateUserCommand { Id = "user-123", Email = "not-an-email" }));
_sut.Validate(new UpdateUserCommand { Id = "user-123", PhoneNumber = new string('1', 16) }));

// Assert — pin the actual Portuguese text, not merely the absence of the English one.
// "does not contain the English sentence" is satisfied by a blank message, by a raw
// resource key leaking through, and by any wrong-but-non-English string, so it stayed
// green through exactly the failures it existed to catch.
var message = result.Errors.Single(e => e.PropertyName == "Email").ErrorMessage;
message.ShouldBe("'Email' é um endereço de email inválido.");
var message = result.Errors.Single(e => e.PropertyName == "PhoneNumber").ErrorMessage;
message.ShouldBe("'Phone Number' deve ser menor ou igual a 15 caracteres. Você digitou 16 caracteres.");
}
}
26 changes: 26 additions & 0 deletions src/Tests/Integration.Tests/Tests/Users/UserProfileTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,32 @@ public async Task UpdateProfile_Should_PersistChanges_When_AuthenticatedUserUpda
dto.PhoneNumber.ShouldBe("1234567890");
}

[Fact]
public async Task UpdateProfile_Should_LeaveEmailUnchanged_When_BodyCarriesAnEmail()
{
// Arrange — e-mail changes need a confirmation flow, so PUT /profile has no email field;
// System.Text.Json skips the unknown property rather than rejecting the request.
using var adminClient = await _auth.CreateRootAdminClientAsync();
var user = await IdentityUserSeeder.CreateLoginableUserAsync(_factory, adminClient, "upd-email");
using var userClient = await _auth.CreateAuthenticatedClientAsync(user.Email, user.Password);

// Act
var response = await userClient.PutAsJsonAsync(
$"{TestConstants.IdentityBasePath}/profile", new
{
firstName = "Renamed",
email = $"changed-{Guid.NewGuid():N}@example.com"
});

// Assert
response.StatusCode.ShouldBe(HttpStatusCode.OK);

var profile = await userClient.GetAsync($"{TestConstants.IdentityBasePath}/profile");
var dto = await profile.DeserializeAsync<UserDto>();
dto.FirstName.ShouldBe("Renamed");
dto.Email.ShouldBe(user.Email);
}

[Fact]
public async Task UpdateProfile_Should_IgnoreSuppliedId_When_DifferentFromAuthenticatedUser()
{
Expand Down
Loading