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

Integração com Certificados (REDIS) #38

Merged
merged 8 commits into from
Apr 1, 2022
179 changes: 131 additions & 48 deletions packages/domains-api/src/controllers/certificates-controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,135 @@
import CertificatesController, {
Request,
// Certificate,
DNSHostedZone
} from './certificates-controller';

test('should create certificate', () => {
// const mockReq: Request<Certificate> = {
// body: {
// event: {
// data: {
// new: {
// domain: "lutarnaoecrime.org",
// id: 33,
// }
// }
// }
// }
// };
const mockReq: Request<DNSHostedZone> = {
body: {
event: {
data: {
new: {
domain_name: "lutarnaoecrime.org",
id: 33,
community_id: 2
// Mock de certificates redis API (Padrão para todos os testes)
const mockCreateWildcard = jest.fn();
const mockCreateRouters = jest.fn();

jest.mock('../redis-db/certificates', () => ({
createWildcard: mockCreateWildcard,
createRouters: mockCreateRouters
}))

import CertificatesController from './certificates-controller';

describe('Certificates controller', () => {
// Mock de clients externos API e Redis (Padrão para todos os testes)
const mockGraphQLClient = {
request: jest.fn()
}

beforeEach(() => {
jest.clearAllMocks();
})

describe('certificate is create', () => {
// Mock de entradas da função (Padrão para testes que criam certificado)
const dns = {
id: 4,
community_id: 1,
domain_name: 'test.org',
ns_ok: true
};
const request = {
body: {
event: {
data: {
new: dns
}
}
}
}
};
// const mockData = {
// dns_hosted_zones_by_pk: {
// hosted_zone: 'aaa.com',
// name_servers: 'aaaaa.com'
// }
// };
// const mockFn = {
// request: jest.fn().mockReturnValue({
// data: mockData,
// errors: {}
// })
// };
const mRes = { status: jest.fn().mockReturnThis(), json: jest.fn(), body: mockReq.body };

const certificatesController = new CertificatesController(jest.fn(), jest.fn());
certificatesController.create(mockReq, mRes);

// expect(mRes.status).toBeCalled();
expect(mRes.body).toBe(mockReq.body);
});
const mockJson = jest.fn();
const response: any = {
status: jest.fn().mockImplementation(() => ({
json: mockJson
}))
}

it('should return graphql response to action', async () => {
const result = {
id: 3,
dns_hosted_zone_id: request.body.event.data.new.id,
is_active: false,
community_id: request.body.event.data.new.community_id,
domain: 'test.org',
}

// Mock de clients externos API e Redis
mockGraphQLClient.request.mockResolvedValueOnce({ mobilizations: [] });
mockGraphQLClient.request.mockResolvedValueOnce({ insert_certificates_one: result });

const certificatesController = new CertificatesController(mockGraphQLClient);
await certificatesController.create(request, response);

expect(response.status.mock.calls[0][0]).toBe(200);
expect(mockJson.mock.calls[0][0]).toBe(result);
});

it('should create traefik router with wildcard in redis', async () => {
mockGraphQLClient.request.mockResolvedValueOnce({ mobilizations: [] });

const certificatesController = new CertificatesController(mockGraphQLClient);
await certificatesController.create(request, response);

const tRouterName = `${request.body.event.data.new.id}-${request.body.event.data.new.domain_name.replace('.', '-')}`;

expect(mockCreateWildcard.mock.calls[0]).toEqual([tRouterName, request.body.event.data.new.domain_name])
});

it('should call fetchMobilizationsByDomain', async () => {
const certificatesController = new CertificatesController(mockGraphQLClient);
await certificatesController.create(request, response);

expect(mockGraphQLClient.request.mock.calls[0][0].variables).toEqual({
domainName: `%${dns.domain_name}%`
});
});

it('should create traefik routers for subdomains in redis', async () => {
const mobilizations = [
{ id: 1, community_id: 2, custom_domain: `www.campaign0.${dns.domain_name}` },
{ id: 2, community_id: 2, custom_domain: `www.campaign1.${dns.domain_name}` }
]

mockGraphQLClient.request.mockResolvedValue({ mobilizations });

const certificatesController = new CertificatesController(mockGraphQLClient);
await certificatesController.create(request, response);
const routerName = `${request.body.event.data.new.id}-${request.body.event.data.new.domain_name.replace('.', '-')}-www`;

expect(mockCreateRouters.mock.calls[0])
.toEqual([routerName, mobilizations.map(m => m.custom_domain)])
});
});

describe('certificate is not create', () => {
// Mock de entradas da função (Padrão para testes que não criam certificado)
const request = {
body: {
event: {
data: {
new: {
id: 4,
community_id: 1,
domain_name: 'test.org',
ns_ok: false
}
}
}
}
}
const mockJson = jest.fn();
const response: any = {
status: jest.fn().mockImplementation(() => ({
json: mockJson
}))
}

it('should return 400 when dns is not ok', async () => {
const certificatesController = new CertificatesController(mockGraphQLClient);
await certificatesController.create(request, response);

expect(response.status.mock.calls[0][0]).toEqual(402);
expect(mockJson.mock.calls[0][0]).toEqual({ message: 'Certificate not created because ns_ok is false.' });
});
});
})

80 changes: 56 additions & 24 deletions packages/domains-api/src/controllers/certificates-controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import logger from '../config/logger';
import { gql } from '../graphql-api/client';
import { createRouters, createWildcard } from '../redis-db/certificates';
import { validationResult, check } from 'express-validator';
import sslChecker from "ssl-checker";

Expand All @@ -14,15 +15,26 @@ export interface Request<T> {
}
}

export interface Certificate {
export interface CertificateTLS {
id: number;
dns_hosted_zone_id: number;
is_active: boolean;
community_id: number;
domain: string;
ssl_checker_response?: any;
}

export interface DNSHostedZone {
id: number;
community_id: number;
domain_name: string;
ns_ok?: boolean;
}

export interface Mobilization {
id: number;
custom_domain: string;
community_id: number;
}

const insert_certificate = gql`mutation ($input: certificates_insert_input!) {
Expand All @@ -32,6 +44,7 @@ const insert_certificate = gql`mutation ($input: certificates_insert_input!) {
is_active
community_id
domain
ssl_checker_response
}
}
`;
Expand All @@ -49,48 +62,56 @@ mutation ($id: Int!, $ssl_checker_response: jsonb) {
}
`;

export const fetch_mobilizations_by_domain = gql`
query ($domainName: String) {
mobilizations (where:{ custom_domain:{ _ilike: $domainName } }) {
id
custom_domain
community_id
}
}
`;

class CertificatesController {
private redisClient: any
private graphqlClient: any

constructor(redisClient, graphqlClient) {
this.redisClient = redisClient;
constructor(graphqlClient) {
this.graphqlClient = graphqlClient;
}

create = async (req: Request<DNSHostedZone>, res) => {
await check('event').isObject().run(req);
// await check('password').isLength({ min: 6 }).run(req);
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
try {
await this.insertCertificateRedis(req.body.event.data.new);
res.status(200).json(await this.insertCertificateGraphql(req.body.event.data.new));
} catch (e: any) {
logger.info(e)
res.status(500).json({ ok: false, ...e });

const dns_hosted_zone = req.body.event.data.new;

if (dns_hosted_zone.ns_ok) {
try {
const domains: string[] = await this.fetchCustomDomains(dns_hosted_zone.domain_name);
await this.insertCertificateRedis(dns_hosted_zone, domains);
res.status(200).json(await this.insertCertificateGraphql(dns_hosted_zone));
} catch (e: any) {
logger.info(e)
res.status(500).json({ ok: false, ...e });
}
} else {
res.status(402).json({ message: 'Certificate not created because ns_ok is false.' });
}
}

private insertCertificateRedis = async (input: any) => {
private insertCertificateRedis = async (input: DNSHostedZone, domains: string[]) => {
const { domain_name, id: dns_hosted_zone_id } = input;
const tRouterName = `${dns_hosted_zone_id}-${domain_name.replace('.', '-')}`
logger.info(`In controller - createCertificate ${tRouterName}`);

await this.redisClient.connect();
await this.redisClient.set(`traefik/http/routers/${tRouterName}/tls`, 'true');
await this.redisClient.set(`traefik/http/routers/${tRouterName}/tls/certresolver`, 'myresolver');
await this.redisClient.set(`traefik/http/routers/${tRouterName}/rule`, `HostRegexp(\`${domain_name}\`, \`{subdomain:.+}.${domain_name}\`)`);
await this.redisClient.set(`traefik/http/routers/${tRouterName}/tls/domains/0/main`, domain_name);
await this.redisClient.set(`traefik/http/routers/${tRouterName}/tls/domains/0/sans/0`, `*.${domain_name}`);
await this.redisClient.set(`traefik/http/routers/${tRouterName}/service`, 'public@docker');
// console.log(await this.redisClient.get('traefik'));
await this.redisClient.quit();

await createWildcard(tRouterName, domain_name);
await createRouters(`${tRouterName}-www`, domains);
}

private insertCertificateGraphql = async (input: any) => {
private insertCertificateGraphql = async (input: any): Promise<CertificateTLS> => {
const { domain_name: domain, community_id, id: dns_hosted_zone_id } = input;
const data: any = await this.graphqlClient.request({
document: insert_certificate,
Expand All @@ -113,7 +134,18 @@ class CertificatesController {
return data.update_certificates_by_pk;
}

check = async (req: Request<Certificate>, res) => {
private fetchCustomDomains = async (domain: string): Promise<string[]> => {
const data: { mobilizations: Mobilization[] } = await this.graphqlClient.request({
document: fetch_mobilizations_by_domain,
variables: { domainName: `%${domain}%` }
});

logger.child({ data }).info('fetch_mobilizations_by_domain');

return data.mobilizations.map((mob) => mob.custom_domain);
}

check = async (req: Request<CertificateTLS>, res) => {
/**
* Esse evento deve ser chamado sempre que criar um novo certificado
* Hasura irá fazer uma nova chamada em caso de erro no intervalo de 6 minutos
Expand Down
Loading