From 8628a5a947dd6a3c2e6008fadc120ddc1b5f9880 Mon Sep 17 00:00:00 2001 From: Matheus Domingos <134434652+DominMFD@users.noreply.github.com> Date: Fri, 20 Sep 2024 14:19:36 -0300 Subject: [PATCH] feat: login twitter service (#189) * feat: login twitter service * fix: add path alias * fix: description text test fixed * feat: implements poo and Unauthorized error * fix: code style fixed * feat: change GenerateAuthURL function for class * chore: add constructor in login twitter service * fix: path alias * fix: adjust error in test --- .../controllers/twitter-controller.test.ts | 67 +++++++++---------- .../twitter/controllers/twitter-controller.ts | 31 ++++----- .../twitter/helpers/generate-auth-url.test.ts | 19 ++++++ .../twitter/helpers/generate-auth-url.ts | 27 ++++---- .../services/login-twitter-service.test.ts | 20 ++++++ .../twitter/services/login-twitter-service.ts | 17 +++++ src/shared/errors/error.test.ts | 13 ++++ .../errors/unauthorized-header-error.ts | 15 +++++ 8 files changed, 143 insertions(+), 66 deletions(-) create mode 100644 src/features/twitter/helpers/generate-auth-url.test.ts create mode 100644 src/features/twitter/services/login-twitter-service.test.ts create mode 100644 src/features/twitter/services/login-twitter-service.ts create mode 100644 src/shared/errors/unauthorized-header-error.ts diff --git a/src/features/twitter/controllers/twitter-controller.test.ts b/src/features/twitter/controllers/twitter-controller.test.ts index 06dae39..7137e4e 100644 --- a/src/features/twitter/controllers/twitter-controller.test.ts +++ b/src/features/twitter/controllers/twitter-controller.test.ts @@ -1,42 +1,24 @@ import type { NextFunction, Request, Response } from 'express'; import { mock, mockDeep } from 'vitest-mock-extended'; -import type { Logger } from '@/shared/infra/logger/logger'; -import { loggerMock } from '@/shared/test-helpers/mocks/logger.mock'; -import { accountRepositoryMock } from '@/shared/test-helpers/mocks/repositories/account-repository.mock'; -import { tokenRepositoryMock } from '@/shared/test-helpers/mocks/repositories/token-repository.mock'; +import type { AuthorizeTwitterService } from '@/features/twitter/services/authorize-twitter-service'; +import type { LoginTwitterService } from '@/features/twitter/services/login-twitter-service'; +import { HttpError } from '@/shared/errors/http-error'; +import { HttpStatusCode } from '@/shared/protocols/http-client'; -import { AuthorizeTwitterService } from '../services/authorize-twitter-service'; -import type { TwitterService } from '../services/twitter-service'; import { TwitterController } from './twitter-controller'; describe('[Controller] Twitter', () => { - let mockLogger: Logger; - let twitterServiceMock: TwitterService; let authorizeTwitterService: AuthorizeTwitterService; + let loginTwitterService: LoginTwitterService; let authController: TwitterController; + let error: HttpError; + let req: Request; let res: Response; let next: NextFunction; - beforeEach(() => { - mockLogger = mock(loggerMock); - - twitterServiceMock = mock({ - getTwitterOAuthToken: vi.fn(), - getTwitterUser: vi.fn(), - }); - - authorizeTwitterService = mock( - new AuthorizeTwitterService( - mockLogger, - twitterServiceMock, - accountRepositoryMock, - tokenRepositoryMock - ) - ); - - authController = new TwitterController(authorizeTwitterService); + beforeEach(() => { req = mockDeep(); res = { @@ -46,6 +28,18 @@ describe('[Controller] Twitter', () => { } as unknown as Response; next = vi.fn() as unknown as NextFunction; + + authorizeTwitterService = mock({ + execute: vi.fn(), + }); + loginTwitterService = mock({ + execute: vi.fn(), + }); + authController = new TwitterController( + authorizeTwitterService, + loginTwitterService + ); + error = new HttpError(HttpStatusCode.serverError, 'error'); }); describe('callback', () => { @@ -54,9 +48,7 @@ describe('[Controller] Twitter', () => { .spyOn(authorizeTwitterService, 'execute') .mockReturnThis(); req.query = { code: '123', state: '123' }; - await authController.callback(req, res, next); - expect(spyAuthorizeTwitter).toHaveBeenCalledWith({ code: '123', state: '123', @@ -66,13 +58,20 @@ describe('[Controller] Twitter', () => { }); describe('login', () => { - it('should be return 401', () => { - req.headers.authorization = undefined; - + it('should be return a URL link on successful login', () => { + vi.spyOn(loginTwitterService, 'execute').mockReturnValue('url'); authController.login(req, res, next); - expect(res.status).toHaveBeenCalledWith(401); - expect(res.json).toHaveBeenCalledWith({ message: 'Unauthorized' }); - }); + expect(res.json).toHaveBeenCalledWith('url'); + }), + it('should be return a error', () => { + vi.spyOn(loginTwitterService, 'execute').mockImplementation(() => { + throw error; + }); + + authController.login(req, res, next); + + expect(next).toHaveBeenCalledWith(error); + }); }); }); diff --git a/src/features/twitter/controllers/twitter-controller.ts b/src/features/twitter/controllers/twitter-controller.ts index 0ac6461..0eac053 100644 --- a/src/features/twitter/controllers/twitter-controller.ts +++ b/src/features/twitter/controllers/twitter-controller.ts @@ -1,12 +1,8 @@ -import jwt from 'jsonwebtoken'; - -import type { TokenPayload } from '@/shared/infra/jwt/jwt'; +import type { AuthorizeTwitterService } from '@/features/twitter/services/authorize-twitter-service'; +import type { LoginTwitterService } from '@/features/twitter/services/login-twitter-service'; import type { Controller } from '@/shared/protocols/controller'; import type { AsyncRequestHandler } from '@/shared/protocols/handlers'; -import { generateAuthURL } from '../helpers/generate-auth-url'; -import type { AuthorizeTwitterService } from '../services/authorize-twitter-service'; - export class TwitterController implements Controller { callback: AsyncRequestHandler = async (req, res) => { const query = req.query; @@ -19,21 +15,18 @@ export class TwitterController implements Controller { return res.send(); }; - login: AsyncRequestHandler = (req, res) => { - const authorization = req.headers.authorization; + login: AsyncRequestHandler = (_, res, next) => { + try { + const url = this.loginTwitter.execute({ userId: '1' }); - if (!authorization) { - return res.status(401).json({ message: 'Unauthorized' }); + return res.json(url); + } catch (err) { + next(err); } - - const [, token] = authorization.split(' '); - - const payload = jwt.verify(token, 'secret_key') as TokenPayload; - - const url = generateAuthURL({ id: payload.userId }); - - return res.json(url); }; - constructor(private readonly authorizeTwitter: AuthorizeTwitterService) {} + constructor( + private readonly authorizeTwitter: AuthorizeTwitterService, + private readonly loginTwitter: LoginTwitterService + ) {} } diff --git a/src/features/twitter/helpers/generate-auth-url.test.ts b/src/features/twitter/helpers/generate-auth-url.test.ts new file mode 100644 index 0000000..f485bca --- /dev/null +++ b/src/features/twitter/helpers/generate-auth-url.test.ts @@ -0,0 +1,19 @@ +import { GenerateAuthURL } from './generate-auth-url'; + +describe('GenerateAuthUrl', () => { + let sut: GenerateAuthURL; + let id: string; + + beforeEach(() => { + sut = new GenerateAuthURL(); + id = '1'; + }); + + it('should return the generated twitter auth URL', () => { + const url = sut.twitter({ id }); + + expect(url).toBe( + `https://twitter.com/i/oauth2/authorize?client_id=undefined&code_challenge=-a4-ROPIVaUBVj1qqB2O6eN_qSC0WvET0EdUEhSFqrI&code_challenge_method=S256&redirect_uri=http%3A%2F%2Fwww.localhost%3A3000%2Fapi%2Ftwitter%2Fcallback&response_type=code&state=${id}&scope=tweet.write%20tweet.read%20users.read` + ); + }); +}); diff --git a/src/features/twitter/helpers/generate-auth-url.ts b/src/features/twitter/helpers/generate-auth-url.ts index e2298cc..0659e4e 100644 --- a/src/features/twitter/helpers/generate-auth-url.ts +++ b/src/features/twitter/helpers/generate-auth-url.ts @@ -3,19 +3,20 @@ import 'dotenv/config'; type Input = { id: string; }; +export class GenerateAuthURL { + twitter({ id }: Input) { + const baseUrl = 'https://twitter.com/i/oauth2/authorize'; + const clientId = process.env.TWITTER_CLIENT_ID!; -export function generateAuthURL({ id }: Input) { - const baseUrl = 'https://twitter.com/i/oauth2/authorize'; - const clientId = process.env.TWITTER_CLIENT_ID!; + const params = new URLSearchParams({ + client_id: clientId, + code_challenge: '-a4-ROPIVaUBVj1qqB2O6eN_qSC0WvET0EdUEhSFqrI', + code_challenge_method: 'S256', + redirect_uri: `http://www.localhost:3000/api/twitter/callback`, + response_type: 'code', + state: id, + }); - const params = new URLSearchParams({ - client_id: clientId, - code_challenge: '-a4-ROPIVaUBVj1qqB2O6eN_qSC0WvET0EdUEhSFqrI', - code_challenge_method: 'S256', - redirect_uri: `http://www.localhost:3000/api/twitter/callback`, - response_type: 'code', - state: id, - }); - - return `${baseUrl}?${params.toString()}&scope=tweet.write%20tweet.read%20users.read`; + return `${baseUrl}?${params.toString()}&scope=tweet.write%20tweet.read%20users.read`; + } } diff --git a/src/features/twitter/services/login-twitter-service.test.ts b/src/features/twitter/services/login-twitter-service.test.ts new file mode 100644 index 0000000..b1a15ab --- /dev/null +++ b/src/features/twitter/services/login-twitter-service.test.ts @@ -0,0 +1,20 @@ +import { GenerateAuthURL } from '../helpers/generate-auth-url'; +import { LoginTwitterService } from './login-twitter-service'; + +describe('LoginTwitterService', () => { + let sut: LoginTwitterService; + let generateAuthUrl: GenerateAuthURL; + let id: string; + + beforeEach(() => { + generateAuthUrl = new GenerateAuthURL(); + sut = new LoginTwitterService(generateAuthUrl); + id = '1'; + }); + + it('should return the generated auth URL', () => { + const result = sut.execute({ userId: id }); + + expect(result).toContain('https://twitter.com/i/oauth2/authorize'); + }); +}); diff --git a/src/features/twitter/services/login-twitter-service.ts b/src/features/twitter/services/login-twitter-service.ts new file mode 100644 index 0000000..d71f36a --- /dev/null +++ b/src/features/twitter/services/login-twitter-service.ts @@ -0,0 +1,17 @@ +import type { Service } from '@/shared/protocols/service'; + +import type { GenerateAuthURL } from '../helpers/generate-auth-url'; + +type Input = { + userId: string; +}; + +export class LoginTwitterService implements Service { + constructor(private readonly generateAuthUrl: GenerateAuthURL) {} + + execute({ userId }: Input) { + const url = this.generateAuthUrl.twitter({ id: userId }); + + return url; + } +} diff --git a/src/shared/errors/error.test.ts b/src/shared/errors/error.test.ts index 36f1fe7..2125a92 100644 --- a/src/shared/errors/error.test.ts +++ b/src/shared/errors/error.test.ts @@ -6,6 +6,8 @@ import { InvalidCredentialsError } from '@/shared/errors/invalid-credentials-err import { UserNotFound } from '@/shared/errors/user-not-found-error'; import { ValidationError } from '@/shared/errors/validation-error'; +import { UnauthorizedHeaderError } from './unauthorized-header-error'; + describe('[Errors]', () => { describe('http-error', () => { it('parses to json correctly', () => { @@ -96,4 +98,15 @@ describe('[Errors]', () => { }); }); }); + + describe('unauthorized-header-error', () => { + it('should parse to json correctly', () => { + const error = new UnauthorizedHeaderError(); + + expect(error.toJSON()).toStrictEqual({ + code: 401, + error: 'Unauthorized', + }); + }); + }); }); diff --git a/src/shared/errors/unauthorized-header-error.ts b/src/shared/errors/unauthorized-header-error.ts new file mode 100644 index 0000000..15f5c1f --- /dev/null +++ b/src/shared/errors/unauthorized-header-error.ts @@ -0,0 +1,15 @@ +import { HttpError } from '@/shared/errors/http-error'; +import { HttpStatusCode } from '@/shared/protocols/http-client'; + +export class UnauthorizedHeaderError extends HttpError { + constructor(public readonly message: string = 'Unauthorized') { + super(HttpStatusCode.unauthorized, message); + } + + public toJSON() { + return { + code: this.code, + error: this.message, + }; + } +}