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
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', () => {
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', () => {

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
Copy link
Contributor

Choose a reason for hiding this comment

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

precisava fazer?

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,
};
}
}
Loading