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
70 changes: 70 additions & 0 deletions src/modules/user/use-cases/sign-in-with-google.use-case.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { GoogleAuthProviderError } from '@/modules/user/errors/google-auth-provider.error'
import { InvalidCredentialsError } from '@/modules/user/errors/invalid-credentials.error'
import { InvalidGoogleTokenError } from '@/modules/user/errors/invalid-google-token.error'
import {
type GoogleAuthProvider,
Expand All @@ -8,6 +9,7 @@ import {
import { InMemoryUsersRepository } from '@/modules/user/repositories/in-memory/in-memory-users.repository'
import { makeUser } from '@/modules/user/tests/factories/make-user'

import { AuthenticateUseCase } from './authenticate.use-case'
import { SignInWithGoogleUseCase } from './sign-in-with-google.use-case'

let usersRepository: InMemoryUsersRepository
Expand Down Expand Up @@ -180,6 +182,74 @@ describe('Sign In With Google Use Case', () => {
expect(signedInUser.avatarUrl).toBe('https://example.com/avatar.png')
})

it('should remove an existing password when linking a verified Google account', async () => {
const user = await makeUser(usersRepository, {
email: 'johndoe@example.com',
name: 'John Doe',
avatarUrl: 'https://example.com/avatar.png',
rawPassword: 'old-password',
})
vi.mocked(googleAuthProvider.getUserInfo).mockResolvedValue({
email: 'johndoe@example.com',
verified_email: true,
name: 'John Doe',
picture: 'https://example.com/avatar.png',
})
const update = vi.spyOn(usersRepository, 'update')

const { user: linkedUser } = await sut.execute({
token: 'google-access-token',
})

expect(linkedUser.password).toBeNull()
expect(update).toHaveBeenCalledWith(user.id, { password: null })
})

it('should not update an unchanged Google-only account', async () => {
await makeUser(usersRepository, {
email: 'johndoe@example.com',
name: 'John Doe',
avatarUrl: 'https://example.com/avatar.png',
password: null,
})
vi.mocked(googleAuthProvider.getUserInfo).mockResolvedValue({
email: 'johndoe@example.com',
verified_email: true,
name: 'John Doe',
picture: 'https://example.com/avatar.png',
})
const update = vi.spyOn(usersRepository, 'update')

await sut.execute({ token: 'google-access-token' })

expect(update).not.toHaveBeenCalled()
})

it('should reject the old password after linking a verified Google account', async () => {
await makeUser(usersRepository, {
email: 'johndoe@example.com',
name: 'John Doe',
avatarUrl: 'https://example.com/avatar.png',
rawPassword: 'old-password',
})
vi.mocked(googleAuthProvider.getUserInfo).mockResolvedValue({
email: 'johndoe@example.com',
verified_email: true,
name: 'John Doe',
picture: 'https://example.com/avatar.png',
})

await sut.execute({ token: 'google-access-token' })

const authenticate = new AuthenticateUseCase(usersRepository)
await expect(
authenticate.execute({
email: 'johndoe@example.com',
password: 'old-password',
})
).rejects.toBeInstanceOf(InvalidCredentialsError)
})

it('should fill an empty name from Google', async () => {
await makeUser(usersRepository, {
email: 'johndoe@example.com',
Expand Down
14 changes: 9 additions & 5 deletions src/modules/user/use-cases/sign-in-with-google.use-case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,17 @@ export class SignInWithGoogleUseCase {
}
}

const needsUpdate =
(!user.name?.trim() && name) || (!user.avatarUrl && picture)
const updateData = {
...(!user.name?.trim() && name && { name }),
...(!user.avatarUrl && picture && { avatarUrl: picture }),
...(user.password !== null && { password: null }),
}

if (needsUpdate) {
if (Object.keys(updateData).length > 0) {
// Temporary pre-hijacking mitigation: a verified Google email proves mailbox ownership.
// Remove the pre-registered password until the app has its own email verification flow.
user = await this.usersRepository.update(user.id, {
...(!user.name?.trim() && name && { name }),
...(!user.avatarUrl && picture && { avatarUrl: picture }),
...updateData,
})
}

Expand Down
Loading