Skip to content
This repository has been archived by the owner on Aug 17, 2019. It is now read-only.

Complete user flow #3

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@
"@types/graphql": "^0.10.2",
"@types/jest": "^20.0.7",
"@types/jsonwebtoken": "^7.2.3",
"@types/kcors": "^2.2.1",
"@types/koa": "^2.0.39",
"@types/koa-bodyparser": "^3.0.23",
"@types/koa-logger": "^2.0.2",
"@types/koa-router": "^7.0.22",
"@types/lodash": "^4.14.73",
"@types/mongodb": "^2.2.10",
"@types/mongoose": "^4.7.20",
Expand All @@ -68,7 +72,8 @@
],
"coveragePathIgnorePatterns": [
"/__tests__/",
"\\.(test|spec)\\.ts$"
"\\.(test|spec)\\.ts$",
"\\.d\\.ts$"
],
"moduleFileExtensions": [
"ts",
Expand Down
60 changes: 60 additions & 0 deletions src/__mocks__/axios.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { omit } from 'lodash';
import { AxiosInstance, AxiosResponse } from 'axios';

interface AxiosMockedInstance extends AxiosInstance {
_setupMock: (params: [MockParams]) => void;
_clean: () => void;
}

type HTTPMethod = 'post' | 'delete' | 'put' | 'get' | 'options';

interface MockParams extends AxiosResponse {
method: HTTPMethod;
url: string;
}

interface MockStructure {
delete: { [key: string]: AxiosResponse };
get: { [key: string]: AxiosResponse };
post: { [key: string]: AxiosResponse };
put: { [key: string]: AxiosResponse };
options: { [key: string]: AxiosResponse };
}

const FROZEN_DATA: MockStructure = {
delete: {},
get: {},
post: {},
put: {},
options: {}
};

const DEFAULT_RESPONSE = {
status: 200
};

let _cached = FROZEN_DATA;

const clean = () => {
_cached = Object.create(FROZEN_DATA);
};

const setupMock = (params: [MockParams]) => {
params.forEach(param => {
_cached[param.method][param.url] = omit<AxiosResponse, MockParams>(param, ['method', 'url']);
});
};

const generateMethod = (method: HTTPMethod) => (
(url: string): Promise<AxiosResponse> => Promise.resolve(_cached[method][url] || DEFAULT_RESPONSE)
);

const axios: AxiosMockedInstance = jest.genMockFromModule('axios');
axios._setupMock = setupMock;
axios._clean = clean;
axios.post = generateMethod('post');
axios.get = generateMethod('get');
axios.delete = generateMethod('delete');
axios.put = generateMethod('put');

export = axios;
53 changes: 53 additions & 0 deletions src/__mocks__/github.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { set } from 'lodash';

type Entity = 'users' | 'repo';

interface MockParams {
data: any;
entity: Entity;
method: string;
resolve: boolean;
}

interface MockResponse {
data: any;
}

interface Handlers {
[key: string]: {
[key: string]: () => Promise<MockResponse>
};
}

let cached: Handlers = {};

class MockedGithub {
auth: any;

constructor () {
const that = this;

Object.keys(cached).forEach(key => {
set(that, key, cached[key]);
});
}

static _setupMock (params: [MockParams]) {
params.forEach(param => {
set(cached, `${param.entity}.${param.method}`, () => param.resolve
? Promise.resolve({ data: param.data })
: Promise.reject({ data: param.data })
);
});
}

static _clean () {
cached = Object.create(null);
}

authenticate (auth: any) {
this.auth = auth;
}
}

export = MockedGithub;
16 changes: 0 additions & 16 deletions src/__tests__/user/__snapshots__/resolvers.ts.snap

This file was deleted.

Empty file added src/declarations.d.ts
Empty file.
8 changes: 8 additions & 0 deletions src/helpers/github-create-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as Github from 'github';
import settings from 'settings';

export default (token: string) => {
const client: Github = new Github({ debug: !settings.isProduction, headers: { 'user-agent': '@flyin/git-chat-server' } });
client.authenticate({ token, type: 'oauth' });
return client;
};
20 changes: 20 additions & 0 deletions src/helpers/github-get-primary-email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { find } from 'lodash';
import * as Github from 'github';
import { GithubResponse } from './interfaces';

type EmailResponse = {
email: string;
primary: boolean;
verified: boolean;
};

export default async (client: Github) => {
const emails: GithubResponse<[EmailResponse]> = await client.users.getEmails({ page: 1, per_page: 100 });
const email = find<EmailResponse>(emails.data, { primary: true, verified: true });

if (!email) {
throw new Error('primary_email_not_found');
}

return email;
};
29 changes: 29 additions & 0 deletions src/helpers/github-get-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { get } from 'lodash';
import settings from 'settings';
import axios from 'axios';

type GithubAccessToken = {
access_token?: string;
scope?: string;
token_type?: string;
error_description?: string;
};

export default async (code: string) => {
const tokenResponse = await axios.post(
'https://github.com/login/oauth/access_token',

{
client_id: settings.github.clientId,
client_secret: settings.github.clientSecret,
code
},
{
headers: {
'Accept': 'application/json'
}
}
);

return get<GithubAccessToken>(tokenResponse, 'data', {});
};
13 changes: 13 additions & 0 deletions src/helpers/github-get-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as Github from 'github';
import { GithubResponse } from './interfaces';

type UserResponse = {
avatar_url?: string;
id: number;
name: string;
};

export default async (client: Github) => {
const user: GithubResponse<UserResponse> = await client.users.get({});
return user.data;
};
5 changes: 5 additions & 0 deletions src/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './interfaces';
export { default as githubCreateClient } from './github-create-client';
export { default as githubGetPrimaryEmail } from './github-get-primary-email';
export { default as githubGetToken } from './github-get-token';
export { default as githubGetUser } from './github-get-user';
3 changes: 3 additions & 0 deletions src/helpers/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface GithubResponse<T> {
data: T;
}
2 changes: 1 addition & 1 deletion src/models/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ const Channel = new Schema({
timestamps: true
});

mongoose.model('Channel', Channel);
export default mongoose.model('Channel', Channel);
6 changes: 3 additions & 3 deletions src/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from './channel';
export * from './message';
export * from './user';
export { default as Channel } from './channel';
export { default as Message } from './message';
export { default as User } from './user';
4 changes: 2 additions & 2 deletions src/models/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as mongoose from 'mongoose';

const Schema = mongoose.Schema;

const schema = new Schema({
const Message = new Schema({
channel: {
_id: {
ref: 'Channel',
Expand All @@ -17,4 +17,4 @@ const schema = new Schema({
timestamps: true
});

mongoose.model('Message', schema);
export default mongoose.model('Message', Message);
15 changes: 7 additions & 8 deletions src/models/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import * as bcrypt from 'bcrypt';
const SALT_FACTOR = 5;

export interface UserModel extends mongoose.Document {
avatar: string;
email: string;
github?: GithubModel;
isAdmin: boolean;
Expand All @@ -14,6 +13,7 @@ export interface UserModel extends mongoose.Document {
}

export interface GithubModel extends mongoose.Document {
avatar: string;
accessToken: string;
githubId: number;
name: string;
Expand All @@ -22,16 +22,15 @@ export interface GithubModel extends mongoose.Document {
}

const Github = new mongoose.Schema({
avatar: { default: null, type: String },
accessToken: { required: true, type: String },
githubId: { required: true, type: Number },
githubId: { required: true, type: Number, index: true },
name: { default: null, type: String },
refreshToken: { default: null, type: String },
scopes: { type: [String] }
});

const user = new mongoose.Schema({
avatar: { default: null, type: String },

const User = new mongoose.Schema({
email: {
required: true,
trim: true,
Expand All @@ -51,7 +50,7 @@ const user = new mongoose.Schema({
timestamps: true
});

user.pre('save', async function (this: UserModel, next) {
User.pre('save', async function (this: UserModel, next) {
if (!this.isModified('password') && this.password) {
return next();
}
Expand All @@ -67,8 +66,8 @@ user.pre('save', async function (this: UserModel, next) {
}
});

user.methods.passwordIsValid = async function (this: UserModel, password: string) {
User.methods.passwordIsValid = async function (this: UserModel, password: string) {
return await bcrypt.compare(password, this.password);
};

mongoose.model<UserModel>('User', user);
export default mongoose.model<UserModel>('User', User);
40 changes: 40 additions & 0 deletions src/resolvers/__tests__/user/__snapshots__/resolvers.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`user resolvers mutation user can attach github to his account 1`] = `
Object {
"email": "[email protected]",
"github": Object {
"avatar": null,
"githubId": "1234",
"name": "flyin",
},
"isAdmin": false,
}
`;

exports[`user resolvers mutation user can register with email and password 1`] = `
Object {
"createUser": Object {
"email": "[email protected]",
"isAdmin": false,
},
}
`;

exports[`user resolvers mutation user can register with github token 1`] = `
Object {
"email": "[email protected]",
"github": Object {
"avatar": null,
"githubId": "1234",
"name": "flyin",
},
"isAdmin": false,
}
`;

exports[`user resolvers mutation user cant register with invalid data 1`] = `
Array [
[GraphQLError: User validation failed: email: Email address \`a@\` is incorrect],
]
`;
Loading