diff --git a/app/Community/Controllers/UserSettingsController.php b/app/Community/Controllers/UserSettingsController.php index afd0bf827f..b99f875034 100644 --- a/app/Community/Controllers/UserSettingsController.php +++ b/app/Community/Controllers/UserSettingsController.php @@ -97,10 +97,16 @@ public function storeUsernameChangeRequest(StoreUsernameChangeRequest $request): /** @var User $user */ $user = $request->user(); - UserUsername::create([ - 'user_id' => $user->id, - 'username' => $data->newDisplayName, - ]); + $isOnlyCapitalizationChange = strtolower($user->display_name) === strtolower($data->newDisplayName); + if ($isOnlyCapitalizationChange) { + $user->display_name = $data->newDisplayName; + $user->save(); + } else { + UserUsername::create([ + 'user_id' => $user->id, + 'username' => $data->newDisplayName, + ]); + } return response()->json(['success' => true]); } diff --git a/app/Community/RouteServiceProvider.php b/app/Community/RouteServiceProvider.php index 363e9085a7..302d792c1e 100755 --- a/app/Community/RouteServiceProvider.php +++ b/app/Community/RouteServiceProvider.php @@ -374,7 +374,7 @@ protected function mapWebRoutes(): void Route::put('email', [UserSettingsController::class, 'updateEmail'])->name('api.settings.email.update'); Route::post('username-change-request', [UserSettingsController::class, 'storeUsernameChangeRequest']) - ->name('api.settings.username-change-request.store'); + ->name('api.settings.name-change-request.store'); Route::delete('keys/web', [UserSettingsController::class, 'resetWebApiKey'])->name('api.settings.keys.web.destroy'); Route::delete('keys/connect', [UserSettingsController::class, 'resetConnectApiKey'])->name('api.settings.keys.connect.destroy'); diff --git a/resources/js/features/settings/components/ChangeUsernameSectionCard/ChangeUsernameSectionCard.test.tsx b/resources/js/features/settings/components/ChangeUsernameSectionCard/ChangeUsernameSectionCard.test.tsx index 07bbe1b474..3d6d6187cb 100644 --- a/resources/js/features/settings/components/ChangeUsernameSectionCard/ChangeUsernameSectionCard.test.tsx +++ b/resources/js/features/settings/components/ChangeUsernameSectionCard/ChangeUsernameSectionCard.test.tsx @@ -8,6 +8,12 @@ import { requestedUsernameAtom } from '../../state/settings.atoms'; import { ChangeUsernameSectionCard } from './ChangeUsernameSectionCard'; describe('Component: ChangeUsernameSectionCard', () => { + const originalLocation = window.location; + + afterEach(() => { + window.location = originalLocation; + }); + it('renders without crashing', () => { // ARRANGE const { container } = render(, { @@ -154,7 +160,7 @@ describe('Component: ChangeUsernameSectionCard', () => { await userEvent.click(screen.getByRole('button', { name: /update/i })); // ASSERT - expect(postSpy).toHaveBeenCalledWith(route('api.settings.username-change-request.store'), { + expect(postSpy).toHaveBeenCalledWith(route('api.settings.name-change-request.store'), { newDisplayName: 'NewName', }); }); @@ -216,4 +222,73 @@ describe('Component: ChangeUsernameSectionCard', () => { expect(screen.getByText(/something went wrong/i)).toBeVisible(); }); }); + + it('given the user submits a username that only differs in case, auto-approves without confirmation', async () => { + // ARRANGE + delete (window as any).location; + window.location = { + ...originalLocation, + reload: vi.fn(), + }; + + const confirmSpy = vi.spyOn(window, 'confirm'); + const reloadSpy = vi.spyOn(window.location, 'reload').mockImplementation(() => {}); + const postSpy = vi.spyOn(axios, 'post').mockResolvedValueOnce({ + data: { + success: true, + }, + }); + + render(, { + pageProps: { + auth: { user: createAuthenticatedUser({ displayName: 'testuser' }) }, + can: { createUsernameChangeRequest: true }, + }, + }); + + // ACT + await userEvent.type(screen.getAllByLabelText(/new username/i)[0], 'TestUser'); + await userEvent.type(screen.getByLabelText(/confirm new username/i), 'TestUser'); + await userEvent.click(screen.getByRole('button', { name: /update/i })); + + // ASSERT + expect(confirmSpy).not.toHaveBeenCalled(); + expect(postSpy).toHaveBeenCalledWith(route('api.settings.name-change-request.store'), { + newDisplayName: 'TestUser', + }); + expect(reloadSpy).toHaveBeenCalled(); + }); + + it('given the API returns success for a case change, reloads the page', async () => { + // ARRANGE + delete (window as any).location; + window.location = { + ...originalLocation, + reload: vi.fn(), + }; + + const reloadSpy = vi.spyOn(window.location, 'reload').mockImplementation(() => {}); + vi.spyOn(axios, 'post').mockResolvedValueOnce({ + data: { + success: true, + }, + }); + + render(, { + pageProps: { + auth: { user: createAuthenticatedUser({ displayName: 'testuser' }) }, + can: { createUsernameChangeRequest: true }, + }, + }); + + // ACT + await userEvent.type(screen.getAllByLabelText(/new username/i)[0], 'TestUser'); + await userEvent.type(screen.getByLabelText(/confirm new username/i), 'TestUser'); + await userEvent.click(screen.getByRole('button', { name: /update/i })); + + // ASSERT + await waitFor(() => { + expect(reloadSpy).toHaveBeenCalled(); + }); + }); }); diff --git a/resources/js/features/settings/components/ChangeUsernameSectionCard/useChangeUsernameForm.ts b/resources/js/features/settings/components/ChangeUsernameSectionCard/useChangeUsernameForm.ts index 394f607336..b7633a7413 100644 --- a/resources/js/features/settings/components/ChangeUsernameSectionCard/useChangeUsernameForm.ts +++ b/resources/js/features/settings/components/ChangeUsernameSectionCard/useChangeUsernameForm.ts @@ -59,11 +59,19 @@ export function useChangeUsernameForm() { const mutation = useMutation({ mutationFn: (formValues: FormValues) => { - return axios.post(route('api.settings.username-change-request.store'), { + return axios.post(route('api.settings.name-change-request.store'), { newDisplayName: formValues.newUsername, }); }, onSuccess: (_, { newUsername }) => { + const wasAutoApproved = auth!.user.displayName.toLowerCase() === newUsername.toLowerCase(); + + if (wasAutoApproved) { + window.location.reload(); + + return; + } + setRequestedUsername(newUsername); }, }); @@ -73,7 +81,12 @@ export function useChangeUsernameForm() { 'You can only request a new username once every 30 days, even if your new username is not approved. Are you sure you want to do this?', ); - if (!confirm(confirmationMessage)) { + // If the user just wants a case change, no need to confirm. + // We'll make the change instantly without needing approval. + const mustShowConfirm = + auth!.user.displayName.toLowerCase() !== formValues.newUsername.toLowerCase(); + + if (mustShowConfirm && !confirm(confirmationMessage)) { return; } diff --git a/resources/js/types/generated.d.ts b/resources/js/types/generated.d.ts index b09137829a..a94fce601f 100644 --- a/resources/js/types/generated.d.ts +++ b/resources/js/types/generated.d.ts @@ -130,8 +130,8 @@ declare namespace App.Community.Data { export type UserSettingsPageProps = { userSettings: App.Data.User; can: App.Data.UserPermissions; - requestedUsername: string | null; displayableRoles: Array; + requestedUsername: string | null; }; } declare namespace App.Community.Enums { diff --git a/resources/js/ziggy.d.ts b/resources/js/ziggy.d.ts index 5b26293693..25a9d7e658 100644 --- a/resources/js/ziggy.d.ts +++ b/resources/js/ziggy.d.ts @@ -581,7 +581,7 @@ declare module 'ziggy-js' { "api.settings.preferences.update": [], "api.settings.password.update": [], "api.settings.email.update": [], - "api.settings.username-change-request.store": [], + "api.settings.name-change-request.store": [], "api.settings.keys.web.destroy": [], "api.settings.keys.connect.destroy": [], "api.active-player.index": [], diff --git a/tests/Feature/Community/Controllers/UserSettingsControllerTest.php b/tests/Feature/Community/Controllers/UserSettingsControllerTest.php index 2cd02fcbd9..3240a11111 100644 --- a/tests/Feature/Community/Controllers/UserSettingsControllerTest.php +++ b/tests/Feature/Community/Controllers/UserSettingsControllerTest.php @@ -125,7 +125,7 @@ public function testUpdateUsername(): void // Act $response = $this->actingAs($user) - ->postJson(route('api.settings.username-change-request.store'), [ + ->postJson(route('api.settings.name-change-request.store'), [ 'newDisplayName' => 'Scott123456712', ]); @@ -139,6 +139,35 @@ public function testUpdateUsername(): void ]); } + public function testUpdateUsernameWithOnlyCapitalizationChange(): void + { + // Arrange + $this->withoutMiddleware(); + + /** @var User $user */ + $user = User::factory()->create([ + 'User' => 'scott', + 'display_name' => 'scott', + ]); + + // Act + $response = $this->actingAs($user) + ->postJson(route('api.settings.name-change-request.store'), [ + 'newDisplayName' => 'Scott', + ]); + + // Assert + $response->assertStatus(200); + + $user = $user->fresh(); + $this->assertEquals('Scott', $user->display_name); // instantly makes the update + + $this->assertDatabaseMissing('user_usernames', [ // does not make an approval request record + 'user_id' => $user->id, + 'username' => 'Scott', + ]); + } + public function testUpdateProfile(): void { // Arrange