Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: login twitter service #189

Merged
merged 11 commits into from
Sep 20, 2024
103 changes: 48 additions & 55 deletions src/features/twitter/controllers/twitter-controller.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Request, Response } from 'express';
import { mock, mockDeep } from 'vitest-mock-extended';

import { LoginTwitterService } from '@/features/twitter/services/login-twitter-service';
import { HttpError } from '@/shared/errors/http-error';
import type { Logger } from '@/shared/infra/logger/logger';
import { HttpStatusCode } from '@/shared/protocols/http-client';
Expand All @@ -9,55 +10,52 @@ import { accountRepositoryMock } from '@/shared/test-helpers/mocks/repositories/
import { tokenRepositoryMock } from '@/shared/test-helpers/mocks/repositories/token-repository.mock';

import { AuthorizeTwitterService } from '../services/authorize-twitter-service';
import { LoginTwitterService } from '../services/login-twitter-service';
import type { TwitterService } from '../services/twitter-service';
import { TwitterController } from './twitter-controller';

const makeSut = () => {
const mockLogger: Logger = mock<Logger>(loggerMock);
const twitterServiceMock = mock<TwitterService>({
getTwitterOAuthToken: vi.fn(),
getTwitterUser: vi.fn(),
});
const authorizeTwitterService = mock<AuthorizeTwitterService>(
new AuthorizeTwitterService(
mockLogger,
twitterServiceMock,
accountRepositoryMock,
tokenRepositoryMock
)
);
describe('[Controller] Twitter', () => {
const makeSut = () => {
const mockLogger: Logger = mock<Logger>(loggerMock);
const twitterServiceMock = mock<TwitterService>({
getTwitterOAuthToken: vi.fn(),
getTwitterUser: vi.fn(),
});
const authorizeTwitterService = mock<AuthorizeTwitterService>(
new AuthorizeTwitterService(
mockLogger,
twitterServiceMock,
accountRepositoryMock,
tokenRepositoryMock
)
);

DominMFD marked this conversation as resolved.
Show resolved Hide resolved
const loginTwitterService = mock<LoginTwitterService>(
new LoginTwitterService({
secretKey: 'secret_key',
})
);
const loginTwitterService = mock<LoginTwitterService>(
new LoginTwitterService()
);

const authController = new TwitterController(
authorizeTwitterService,
loginTwitterService
);
const authController = new TwitterController(
authorizeTwitterService,
loginTwitterService
);

const req = mockDeep<Request>();
const res = {
json: vi.fn(),
send: vi.fn(),
status: vi.fn().mockReturnThis(),
} as unknown as Response;
const next = vi.fn();
return {
authController,
authorizeTwitterService,
loginTwitterService,
mockLogger,
next,
req,
res,
twitterServiceMock,
const req = mockDeep<Request>();
const res = {
json: vi.fn(),
send: vi.fn(),
status: vi.fn().mockReturnThis(),
} as unknown as Response;
const next = vi.fn();
return {
authController,
authorizeTwitterService,
loginTwitterService,
mockLogger,
next,
req,
res,
twitterServiceMock,
};
};
DominMFD marked this conversation as resolved.
Show resolved Hide resolved
};
describe('[Controller] Twitter', () => {
describe('callback', () => {
it('should be return code', async () => {
const { authController, authorizeTwitterService, next, req, res } =
Expand All @@ -79,23 +77,18 @@ describe('[Controller] Twitter', () => {
it('should be return a URL link on successful login', () => {
const { authController, loginTwitterService, next, req, res } = makeSut();

req.headers.authorization = 'Bearer token';

const serviceSpy = vi
.spyOn(loginTwitterService, 'execute')
.mockReturnValue('url');
vi.spyOn(loginTwitterService, 'execute').mockReturnValue('url');
authController.login(req, res, next);

expect(serviceSpy).toHaveBeenCalledWith({
authorization: 'Bearer token',
});
expect(res.json).toHaveBeenCalledWith('url');
}),
it('should return 401 if authorization header is missing', () => {
const { authController, next, req, res } = makeSut();
const error = new HttpError(HttpStatusCode.badRequest, 'Unauthorized');

req.headers.authorization = undefined;
it('should be return a error', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
it('should be return a error', () => {
it('should return a error', () => {

const { authController, loginTwitterService, next, req, res } =
makeSut();
const error = new HttpError(HttpStatusCode.badRequest, 'Message Error');
DominMFD marked this conversation as resolved.
Show resolved Hide resolved
vi.spyOn(loginTwitterService, 'execute').mockImplementation(() => {
throw error;
});

authController.login(req, res, next);

Expand Down
6 changes: 2 additions & 4 deletions src/features/twitter/controllers/twitter-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,9 @@ export class TwitterController implements Controller {
return res.send();
};

login: AsyncRequestHandler = (req, res, next) => {
login: AsyncRequestHandler = (_, res, next) => {
try {
const url = this.loginTwitter.execute({
authorization: req.headers.authorization,
});
const url = this.loginTwitter.execute({ userId: '1' });

return res.json(url);
} catch (err) {
Expand Down
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`;
}
}
42 changes: 4 additions & 38 deletions src/features/twitter/services/login-twitter-service.test.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,17 @@
import jwt from 'jsonwebtoken';
import type { Mock } from 'vitest';

import { InvalidCredentialsError } from '@/shared/errors/invalid-credentials-error';
import { JWTHelper } from '@/shared/infra/jwt/jwt';

import { LoginTwitterService } from './login-twitter-service';

vi.mock('jsonwebtoken', () => ({
default: {
verify: vi.fn(() => ({ userId: '123' })),
},
}));

describe('LoginTwitterService', () => {
let sut: LoginTwitterService;
let mockVerify: Mock;
const jsonwebtoken = jwt;
let id: string;

beforeEach(() => {
sut = new LoginTwitterService({
secretKey: 'secret_key',
});
mockVerify = vi.fn(() => ({ userId: `123` }));
jsonwebtoken.verify = mockVerify;
sut = new LoginTwitterService();
id = '1';
});

it('should return the generated auth URL', () => {
const authorization = 'Bearer token';
DominMFD marked this conversation as resolved.
Show resolved Hide resolved
const result = sut.execute({ authorization });
const result = sut.execute({ userId: id });

expect(mockVerify).toHaveBeenCalledWith('token', 'secret_key');
expect(result).toContain('https://twitter.com/i/oauth2/authorize');
});

it('should throw an error if authorization header is missing', () => {
expect(() => sut.execute({ authorization: '' })).toThrow('Unauthorized');
});

it('should throw an error if invalid token', () => {
vi.spyOn(JWTHelper.prototype, 'parseToken').mockImplementation(() => {
throw new Error('Invalid token');
});

const authorization = `Bearer invalid_token`;

expect(() => sut.execute({ authorization })).toThrow(
InvalidCredentialsError
);
});
});
36 changes: 5 additions & 31 deletions src/features/twitter/services/login-twitter-service.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,15 @@
import { InvalidCredentialsError } from '@/shared/errors/invalid-credentials-error';
import { UnauthorizedHeaderError } from '@/shared/errors/unauthorized-header-error';
import { JWTHelper } from '@/shared/infra/jwt/jwt';
import type { Service } from '@/shared/protocols/service';

import { generateAuthURL } from '../helpers/generate-auth-url';

type AuthConfig = {
secretKey: string;
};
import { GenerateAuthURL } from '../helpers/generate-auth-url';

type Input = {
authorization?: string;
userId: string;
};

export class LoginTwitterService implements Service<Input, string> {
private authConfig: AuthConfig;

constructor(authConfig: AuthConfig) {
this.authConfig = authConfig;
}

execute({ authorization }: Input) {
if (!authorization) {
throw new UnauthorizedHeaderError();
}

const jwtHelper = new JWTHelper(this.authConfig.secretKey);
const [, token] = authorization.split(' ');
let payload;

try {
payload = jwtHelper.parseToken(token);
} catch {
throw new InvalidCredentialsError();
}

const url = generateAuthURL({ id: payload.userId });
execute({ userId }: Input) {
const generateAuthURL = new GenerateAuthURL();
DominMFD marked this conversation as resolved.
Show resolved Hide resolved
const url = generateAuthURL.twitter({ id: userId });

return url;
}
Expand Down
Loading