Skip to content

Commit

Permalink
feat: login twitter service (#189)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
DominMFD authored Sep 20, 2024
1 parent f970ef0 commit 8628a5a
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 66 deletions.
67 changes: 33 additions & 34 deletions src/features/twitter/controllers/twitter-controller.test.ts
Original file line number Diff line number Diff line change
@@ -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<Logger>(loggerMock);

twitterServiceMock = mock<TwitterService>({
getTwitterOAuthToken: vi.fn(),
getTwitterUser: vi.fn(),
});

authorizeTwitterService = mock<AuthorizeTwitterService>(
new AuthorizeTwitterService(
mockLogger,
twitterServiceMock,
accountRepositoryMock,
tokenRepositoryMock
)
);

authController = new TwitterController(authorizeTwitterService);

beforeEach(() => {
req = mockDeep<Request>();

res = {
Expand All @@ -46,6 +28,18 @@ describe('[Controller] Twitter', () => {
} as unknown as Response;

next = vi.fn() as unknown as NextFunction;

authorizeTwitterService = mock<AuthorizeTwitterService>({
execute: vi.fn(),
});
loginTwitterService = mock<LoginTwitterService>({
execute: vi.fn(),
});
authController = new TwitterController(
authorizeTwitterService,
loginTwitterService
);
error = new HttpError(HttpStatusCode.serverError, 'error');
});

describe('callback', () => {
Expand All @@ -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',
Expand All @@ -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);
});
});
});
31 changes: 12 additions & 19 deletions src/features/twitter/controllers/twitter-controller.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
) {}
}
19 changes: 19 additions & 0 deletions src/features/twitter/helpers/generate-auth-url.test.ts
Original file line number Diff line number Diff line change
@@ -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`
);
});
});
27 changes: 14 additions & 13 deletions src/features/twitter/helpers/generate-auth-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
}
}
20 changes: 20 additions & 0 deletions src/features/twitter/services/login-twitter-service.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
17 changes: 17 additions & 0 deletions src/features/twitter/services/login-twitter-service.ts
Original file line number Diff line number Diff line change
@@ -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<Input, string> {
constructor(private readonly generateAuthUrl: GenerateAuthURL) {}

execute({ userId }: Input) {
const url = this.generateAuthUrl.twitter({ id: userId });

return url;
}
}
13 changes: 13 additions & 0 deletions src/shared/errors/error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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',
});
});
});
});
15 changes: 15 additions & 0 deletions src/shared/errors/unauthorized-header-error.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}
}

0 comments on commit 8628a5a

Please sign in to comment.