Added Axios + Logging & Testing Frameworks.
Rewrote mojang.ts to use axios. This included creating a more robust error handling system and response payload structure. Also included unit tests. Added axios (HTTP Library to replace request) Added winston (Logging Framework) Added mocha (Testing Framework) Added chai (assertion library) Added nock (mock server)
This commit is contained in:
parent
761a46060b
commit
9097bafb5d
856
package-lock.json
generated
856
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
@ -22,7 +22,8 @@
|
||||
"dist:linux": "npm run dist -- LINUX",
|
||||
"lint": "eslint --ext=jsx,js,tsx,ts src",
|
||||
"dev": "electron-webpack dev",
|
||||
"compile": "electron-webpack"
|
||||
"compile": "electron-webpack",
|
||||
"test": "cross-env TS_NODE_PROJECT='./tsconfig.test.json' mocha -r ts-node/register test/**/*.ts"
|
||||
},
|
||||
"engines": {
|
||||
"node": "12.x.x"
|
||||
@ -30,29 +31,37 @@
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.4.14",
|
||||
"async": "^3.2.0",
|
||||
"axios": "^0.19.2",
|
||||
"discord-rpc": "3.1.0",
|
||||
"electron-updater": "^4.2.4",
|
||||
"fs-extra": "^9.0.0",
|
||||
"github-syntax-dark": "^0.5.0",
|
||||
"jquery": "^3.4.1",
|
||||
"jquery": "^3.5.0",
|
||||
"moment": "^2.24.0",
|
||||
"request": "^2.88.2",
|
||||
"semver": "^7.1.3",
|
||||
"semver": "^7.2.2",
|
||||
"tar-fs": "^2.0.0",
|
||||
"winreg": "^1.2.4"
|
||||
"triple-beam": "^1.3.0",
|
||||
"winreg": "^1.2.4",
|
||||
"winston": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-react": "^7.9.4",
|
||||
"@types/adm-zip": "^0.4.32",
|
||||
"@types/adm-zip": "^0.4.33",
|
||||
"@types/async": "^3.0.8",
|
||||
"@types/chai": "^4.2.11",
|
||||
"@types/discord-rpc": "^3.0.2",
|
||||
"@types/fs-extra": "^8.1.0",
|
||||
"@types/jquery": "^3.3.33",
|
||||
"@types/mocha": "^7.0.2",
|
||||
"@types/node": "^12.12.29",
|
||||
"@types/react": "^16.9.23",
|
||||
"@types/react-dom": "^16.9.5",
|
||||
"@types/request": "^2.48.4",
|
||||
"@types/tar-fs": "^1.16.2",
|
||||
"@types/triple-beam": "^1.3.0",
|
||||
"@types/winreg": "^1.2.30",
|
||||
"chai": "^4.2.0",
|
||||
"cross-env": "^7.0.2",
|
||||
"electron": "^8.2.1",
|
||||
"electron-builder": "^22.4.0",
|
||||
@ -60,10 +69,13 @@
|
||||
"electron-webpack-ts": "^4.0.1",
|
||||
"eslint": "^6.8.0",
|
||||
"helios-distribution-types": "1.0.0-pre.1",
|
||||
"mocha": "^7.1.1",
|
||||
"nock": "^12.0.3",
|
||||
"react": "^16.13.0",
|
||||
"react-dom": "^16.13.0",
|
||||
"react-hot-loader": "^4.12.19",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-node": "^8.8.2",
|
||||
"typescript": "^3.8.3",
|
||||
"webpack": "^4.42.0"
|
||||
},
|
||||
|
40
src/main/logging/loggerutil.ts
Normal file
40
src/main/logging/loggerutil.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { createLogger, format, transports } from 'winston'
|
||||
import { SPLAT } from 'triple-beam'
|
||||
import moment from 'moment'
|
||||
import { inspect } from 'util'
|
||||
|
||||
export class LoggerUtil {
|
||||
|
||||
public static getLogger(label: string) {
|
||||
return createLogger({
|
||||
format: format.combine(
|
||||
format.label(),
|
||||
format.colorize(),
|
||||
format.label({ label }),
|
||||
format.printf(info => {
|
||||
if(info[SPLAT]) {
|
||||
if(info[SPLAT].length === 1 && info[SPLAT][0] instanceof Error) {
|
||||
const err = info[SPLAT][0] as Error
|
||||
if(info.message.length > err.message.length && info.message.endsWith(err.message)) {
|
||||
info.message = info.message.substring(0, info.message.length-err.message.length)
|
||||
}
|
||||
} else if(info[SPLAT].length > 0) {
|
||||
info.message += ' ' + info[SPLAT].map((it: any) => {
|
||||
if(typeof it === 'object' && it != null) {
|
||||
return inspect(it, false, null, true)
|
||||
}
|
||||
return it
|
||||
}).join(' ')
|
||||
}
|
||||
}
|
||||
return `[${moment().format('YYYY-MM-DD hh:mm:ss').trim()}] [${info.level}] [${info.label}]: ${info.message}${info.stack ? `\n${info.stack}` : ''}`
|
||||
})
|
||||
),
|
||||
level: 'debug',
|
||||
transports: [
|
||||
new transports.Console()
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
}
|
@ -1,15 +1,20 @@
|
||||
import request from 'request'
|
||||
import { LoggerUtil } from '../loggerutil'
|
||||
import { LoggerUtil } from '../logging/loggerutil'
|
||||
import { Agent } from '../model/mojang/auth/Agent'
|
||||
import { AuthPayload } from '../model/mojang/auth/AuthPayload'
|
||||
import { Status, StatusColor } from './type/Status'
|
||||
import axios, { AxiosError } from 'axios'
|
||||
import { Session } from '../model/mojang/auth/Session'
|
||||
import { Status } from './type/Status'
|
||||
import { AuthPayload } from '../model/mojang/auth/AuthPayload'
|
||||
import { MojangResponse, MojangResponseCode, deciperResponseCode, isInternalError } from './type/Response'
|
||||
|
||||
export class Mojang {
|
||||
|
||||
private static readonly logger = new LoggerUtil('%c[Mojang]', 'color: #a02d2a; font-weight: bold')
|
||||
private static readonly logger = LoggerUtil.getLogger('Mojang')
|
||||
|
||||
private static readonly TIMEOUT = 2500
|
||||
|
||||
public static readonly AUTH_ENDPOINT = 'https://authserver.mojang.com'
|
||||
public static readonly STATUS_ENDPOINT = 'https://status.mojang.com/check'
|
||||
|
||||
public static readonly MINECRAFT_AGENT: Agent = {
|
||||
name: 'Minecraft',
|
||||
version: 1
|
||||
@ -18,37 +23,37 @@ export class Mojang {
|
||||
protected static statuses: Status[] = [
|
||||
{
|
||||
service: 'sessionserver.mojang.com',
|
||||
status: 'grey',
|
||||
status: StatusColor.GREY,
|
||||
name: 'Multiplayer Session Service',
|
||||
essential: true
|
||||
},
|
||||
{
|
||||
service: 'authserver.mojang.com',
|
||||
status: 'grey',
|
||||
status: StatusColor.GREY,
|
||||
name: 'Authentication Service',
|
||||
essential: true
|
||||
},
|
||||
{
|
||||
service: 'textures.minecraft.net',
|
||||
status: 'grey',
|
||||
status: StatusColor.GREY,
|
||||
name: 'Minecraft Skins',
|
||||
essential: false
|
||||
},
|
||||
{
|
||||
service: 'api.mojang.com',
|
||||
status: 'grey',
|
||||
status: StatusColor.GREY,
|
||||
name: 'Public API',
|
||||
essential: false
|
||||
},
|
||||
{
|
||||
service: 'minecraft.net',
|
||||
status: 'grey',
|
||||
status: StatusColor.GREY,
|
||||
name: 'Minecraft.net',
|
||||
essential: false
|
||||
},
|
||||
{
|
||||
service: 'account.mojang.com',
|
||||
status: 'grey',
|
||||
status: StatusColor.GREY,
|
||||
name: 'Mojang Accounts Website',
|
||||
essential: false
|
||||
}
|
||||
@ -58,24 +63,50 @@ export class Mojang {
|
||||
* Converts a Mojang status color to a hex value. Valid statuses
|
||||
* are 'green', 'yellow', 'red', and 'grey'. Grey is a custom status
|
||||
* to our project which represents an unknown status.
|
||||
*
|
||||
* @param {string} status A valid status code.
|
||||
* @returns {string} The hex color of the status code.
|
||||
*/
|
||||
public static statusToHex(status: string){
|
||||
switch(status.toLowerCase()){
|
||||
case 'green':
|
||||
case StatusColor.GREEN:
|
||||
return '#a5c325'
|
||||
case 'yellow':
|
||||
case StatusColor.YELLOW:
|
||||
return '#eac918'
|
||||
case 'red':
|
||||
case StatusColor.RED:
|
||||
return '#c32625'
|
||||
case 'grey':
|
||||
case StatusColor.GREY:
|
||||
default:
|
||||
return '#848484'
|
||||
}
|
||||
}
|
||||
|
||||
private static handleAxiosError<T>(operation: string, error: AxiosError, dataProvider: () => T): MojangResponse<T> {
|
||||
const response: MojangResponse<T> = {
|
||||
data: dataProvider(),
|
||||
responseCode: MojangResponseCode.ERROR,
|
||||
error
|
||||
}
|
||||
|
||||
if(error.response) {
|
||||
response.responseCode = deciperResponseCode(error.response.data)
|
||||
Mojang.logger.error(`Error during ${operation} request (HTTP Response ${error.response.status})`, error)
|
||||
Mojang.logger.debug('Response Details:')
|
||||
Mojang.logger.debug('Data:', error.response.data)
|
||||
Mojang.logger.debug('Headers:', error.response.headers)
|
||||
} else if(error.request) {
|
||||
Mojang.logger.error(`${operation} request recieved no response.`, error)
|
||||
} else {
|
||||
Mojang.logger.error(`Error during ${operation} request.`, error)
|
||||
}
|
||||
response.isInternalError = isInternalError(response.responseCode)
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
private static expectSpecificSuccess(operation: string, expected: number, actual: number) {
|
||||
if(actual !== expected) {
|
||||
Mojang.logger.warn(`${operation} expected ${expected} response, recieved ${actual}.`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the status of Mojang's services.
|
||||
* The response is condensed into a single object. Each service is
|
||||
@ -84,38 +115,38 @@ export class Mojang {
|
||||
*
|
||||
* @see http://wiki.vg/Mojang_API#API_Status
|
||||
*/
|
||||
public static status(): Promise<Status[]>{
|
||||
return new Promise((resolve, reject) => {
|
||||
request.get('https://status.mojang.com/check',
|
||||
{
|
||||
json: true,
|
||||
timeout: 2500
|
||||
},
|
||||
function(error, response, body: {[service: string]: 'red' | 'yellow' | 'green'}[]){
|
||||
public static async status(): Promise<MojangResponse<Status[]>>{
|
||||
try {
|
||||
|
||||
if(error || response.statusCode !== 200){
|
||||
Mojang.logger.warn('Unable to retrieve Mojang status.')
|
||||
Mojang.logger.debug('Error while retrieving Mojang statuses:', error)
|
||||
//reject(error || response.statusCode)
|
||||
for(let i=0; i<Mojang.statuses.length; i++){
|
||||
Mojang.statuses[i].status = 'grey'
|
||||
}
|
||||
resolve(Mojang.statuses)
|
||||
} else {
|
||||
for(let i=0; i<body.length; i++){
|
||||
const key = Object.keys(body[i])[0]
|
||||
inner:
|
||||
for(let j=0; j<Mojang.statuses.length; j++){
|
||||
if(Mojang.statuses[j].service === key) {
|
||||
Mojang.statuses[j].status = body[i][key]
|
||||
break inner
|
||||
}
|
||||
}
|
||||
}
|
||||
resolve(Mojang.statuses)
|
||||
const res = await axios.get<{[service: string]: StatusColor}[]>(Mojang.STATUS_ENDPOINT, { timeout: Mojang.TIMEOUT })
|
||||
|
||||
Mojang.expectSpecificSuccess('Mojang Status', 200, res.status)
|
||||
|
||||
res.data.forEach(status => {
|
||||
const entry = Object.entries(status)[0]
|
||||
for(let i=0; i<Mojang.statuses.length; i++) {
|
||||
if(Mojang.statuses[i].service === entry[0]) {
|
||||
Mojang.statuses[i].status = entry[1]
|
||||
break
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
data: Mojang.statuses,
|
||||
responseCode: MojangResponseCode.SUCCESS
|
||||
}
|
||||
|
||||
} catch(error) {
|
||||
|
||||
return Mojang.handleAxiosError('Mojang Status', error as AxiosError, () => {
|
||||
for(let i=0; i<Mojang.statuses.length; i++){
|
||||
Mojang.statuses[i].status = StatusColor.GREY
|
||||
}
|
||||
return Mojang.statuses
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -129,43 +160,37 @@ export class Mojang {
|
||||
*
|
||||
* @see http://wiki.vg/Authentication#Authenticate
|
||||
*/
|
||||
public static authenticate(
|
||||
public static async authenticate(
|
||||
username: string,
|
||||
password: string,
|
||||
clientToken: string | null,
|
||||
requestUser: boolean = true,
|
||||
agent: Agent = Mojang.MINECRAFT_AGENT
|
||||
): Promise<Session> {
|
||||
return new Promise((resolve, reject) => {
|
||||
): Promise<MojangResponse<Session | null>> {
|
||||
|
||||
const body: AuthPayload = {
|
||||
try {
|
||||
|
||||
const data: AuthPayload = {
|
||||
agent,
|
||||
username,
|
||||
password,
|
||||
requestUser
|
||||
}
|
||||
if(clientToken != null){
|
||||
body.clientToken = clientToken
|
||||
data.clientToken = clientToken
|
||||
}
|
||||
|
||||
request.post(Mojang.AUTH_ENDPOINT + '/authenticate',
|
||||
{
|
||||
json: true,
|
||||
body
|
||||
},
|
||||
function(error, response, body){
|
||||
if(error){
|
||||
Mojang.logger.error('Error during authentication.', error)
|
||||
reject(error)
|
||||
} else {
|
||||
if(response.statusCode === 200){
|
||||
resolve(body)
|
||||
} else {
|
||||
reject(body || {code: 'ENOTFOUND'})
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
const res = await axios.post<Session>(`${Mojang.AUTH_ENDPOINT}/authenticate`, data)
|
||||
Mojang.expectSpecificSuccess('Mojang Authenticate', 200, res.status)
|
||||
return {
|
||||
data: res.data,
|
||||
responseCode: MojangResponseCode.SUCCESS
|
||||
}
|
||||
|
||||
} catch(err) {
|
||||
return Mojang.handleAxiosError('Mojang Authenticate', err, () => null)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -177,30 +202,33 @@ export class Mojang {
|
||||
*
|
||||
* @see http://wiki.vg/Authentication#Validate
|
||||
*/
|
||||
public static validate(accessToken: string, clientToken: string): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
request.post(Mojang.AUTH_ENDPOINT + '/validate',
|
||||
{
|
||||
json: true,
|
||||
body: {
|
||||
accessToken,
|
||||
clientToken
|
||||
}
|
||||
},
|
||||
function(error, response, body){
|
||||
if(error){
|
||||
Mojang.logger.error('Error during validation.', error)
|
||||
reject(error)
|
||||
} else {
|
||||
if(response.statusCode === 403){
|
||||
resolve(false)
|
||||
} else {
|
||||
// 204 if valid
|
||||
resolve(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
public static async validate(accessToken: string, clientToken: string): Promise<MojangResponse<boolean>> {
|
||||
|
||||
try {
|
||||
|
||||
const data = {
|
||||
accessToken,
|
||||
clientToken
|
||||
}
|
||||
|
||||
const res = await axios.post(`${Mojang.AUTH_ENDPOINT}/validate`, data)
|
||||
Mojang.expectSpecificSuccess('Mojang Validate', 204, res.status)
|
||||
|
||||
return {
|
||||
data: res.status === 204,
|
||||
responseCode: MojangResponseCode.SUCCESS
|
||||
}
|
||||
|
||||
} catch(err) {
|
||||
if(err.response && err.response.status === 403) {
|
||||
return {
|
||||
data: false,
|
||||
responseCode: MojangResponseCode.SUCCESS
|
||||
}
|
||||
}
|
||||
return Mojang.handleAxiosError('Mojang Validate', err, () => false)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -212,29 +240,27 @@ export class Mojang {
|
||||
*
|
||||
* @see http://wiki.vg/Authentication#Invalidate
|
||||
*/
|
||||
public static invalidate(accessToken: string, clientToken: string): Promise<void>{
|
||||
return new Promise((resolve, reject) => {
|
||||
request.post(Mojang.AUTH_ENDPOINT + '/invalidate',
|
||||
{
|
||||
json: true,
|
||||
body: {
|
||||
accessToken,
|
||||
clientToken
|
||||
}
|
||||
},
|
||||
function(error, response, body){
|
||||
if(error){
|
||||
Mojang.logger.error('Error during invalidation.', error)
|
||||
reject(error)
|
||||
} else {
|
||||
if(response.statusCode === 204){
|
||||
resolve()
|
||||
} else {
|
||||
reject(body)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
public static async invalidate(accessToken: string, clientToken: string): Promise<MojangResponse<undefined>> {
|
||||
|
||||
try {
|
||||
|
||||
const data = {
|
||||
accessToken,
|
||||
clientToken
|
||||
}
|
||||
|
||||
const res = await axios.post(`${Mojang.AUTH_ENDPOINT}/invalidate`, data)
|
||||
Mojang.expectSpecificSuccess('Mojang Invalidate', 204, res.status)
|
||||
|
||||
return {
|
||||
data: undefined,
|
||||
responseCode: MojangResponseCode.SUCCESS
|
||||
}
|
||||
|
||||
} catch(err) {
|
||||
return Mojang.handleAxiosError('Mojang Invalidate', err, () => undefined)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -248,30 +274,28 @@ export class Mojang {
|
||||
*
|
||||
* @see http://wiki.vg/Authentication#Refresh
|
||||
*/
|
||||
public static refresh(accessToken: string, clientToken: string, requestUser: boolean = true): Promise<Session> {
|
||||
return new Promise((resolve, reject) => {
|
||||
request.post(Mojang.AUTH_ENDPOINT + '/refresh',
|
||||
{
|
||||
json: true,
|
||||
body: {
|
||||
accessToken,
|
||||
clientToken,
|
||||
requestUser
|
||||
}
|
||||
},
|
||||
function(error, response, body){
|
||||
if(error){
|
||||
Mojang.logger.error('Error during refresh.', error)
|
||||
reject(error)
|
||||
} else {
|
||||
if(response.statusCode === 200){
|
||||
resolve(body)
|
||||
} else {
|
||||
reject(body)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
public static async refresh(accessToken: string, clientToken: string, requestUser: boolean = true): Promise<MojangResponse<Session | null>> {
|
||||
|
||||
try {
|
||||
|
||||
const data = {
|
||||
accessToken,
|
||||
clientToken,
|
||||
requestUser
|
||||
}
|
||||
|
||||
const res = await axios.post<Session>(`${Mojang.AUTH_ENDPOINT}/refresh`, data)
|
||||
Mojang.expectSpecificSuccess('Mojang Refresh', 200, res.status)
|
||||
|
||||
return {
|
||||
data: res.data,
|
||||
responseCode: MojangResponseCode.SUCCESS
|
||||
}
|
||||
|
||||
} catch(err) {
|
||||
return Mojang.handleAxiosError('Mojang Refresh', err, () => null)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
87
src/main/mojang/type/Response.ts
Normal file
87
src/main/mojang/type/Response.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { AxiosError } from "axios"
|
||||
|
||||
/**
|
||||
* @see https://wiki.vg/Authentication#Errors
|
||||
*/
|
||||
export enum MojangResponseCode {
|
||||
SUCCESS,
|
||||
ERROR,
|
||||
ERROR_METHOD_NOT_ALLOWED, // INTERNAL
|
||||
ERROR_NOT_FOUND, // INTERNAL
|
||||
ERROR_USER_MIGRATED,
|
||||
ERROR_INVALID_CREDENTIALS,
|
||||
ERROR_RATELIMIT,
|
||||
ERROR_INVALID_TOKEN,
|
||||
ERROR_ACCESS_TOKEN_HAS_PROFILE, // ??
|
||||
ERROR_CREDENTIALS_ARE_NULL, // INTERNAL
|
||||
ERROR_INVALID_SALT_VERSION, // ??
|
||||
ERROR_UNSUPPORTED_MEDIA_TYPE // INTERNAL
|
||||
}
|
||||
|
||||
export interface MojangResponse<T> {
|
||||
|
||||
data: T
|
||||
responseCode: MojangResponseCode
|
||||
error?: AxiosError
|
||||
isInternalError?: boolean
|
||||
|
||||
}
|
||||
|
||||
export function deciperResponseCode(body: { error: string, errorMessage: string, cause?: string }): MojangResponseCode {
|
||||
|
||||
if(body.error === 'Method Not Allowed') {
|
||||
return MojangResponseCode.ERROR_METHOD_NOT_ALLOWED
|
||||
} else if(body.error === 'Not Found') {
|
||||
return MojangResponseCode.ERROR_NOT_FOUND
|
||||
} else if(body.error === 'Unsupported Media Type') {
|
||||
return MojangResponseCode.ERROR_UNSUPPORTED_MEDIA_TYPE
|
||||
} else if(body.error === 'ForbiddenOperationException') {
|
||||
|
||||
if(body.cause && body.cause === 'UserMigratedException') {
|
||||
return MojangResponseCode.ERROR_USER_MIGRATED
|
||||
}
|
||||
|
||||
if(body.errorMessage === 'Invalid credentials. Invalid username or password.') {
|
||||
return MojangResponseCode.ERROR_INVALID_CREDENTIALS
|
||||
} else if(body.errorMessage === 'Invalid credentials.') {
|
||||
return MojangResponseCode.ERROR_RATELIMIT
|
||||
} else if(body.errorMessage === 'Invalid token.') {
|
||||
return MojangResponseCode.ERROR_INVALID_TOKEN
|
||||
}
|
||||
|
||||
} else if(body.error === 'IllegalArgumentException') {
|
||||
|
||||
if(body.errorMessage === 'Access token already has a profile assigned.') {
|
||||
return MojangResponseCode.ERROR_ACCESS_TOKEN_HAS_PROFILE
|
||||
} else if(body.errorMessage === 'credentials is null') {
|
||||
return MojangResponseCode.ERROR_CREDENTIALS_ARE_NULL
|
||||
} else if(body.errorMessage === 'Invalid salt version') {
|
||||
return MojangResponseCode.ERROR_INVALID_SALT_VERSION
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return MojangResponseCode.ERROR
|
||||
|
||||
}
|
||||
|
||||
// These indicate problems with the code and not the data.
|
||||
export function isInternalError(responseCode: MojangResponseCode): boolean {
|
||||
switch(responseCode) {
|
||||
// We've sent the wrong method to an endpoint. (ex. GET to POST)
|
||||
case MojangResponseCode.ERROR_METHOD_NOT_ALLOWED:
|
||||
// Indicates endpoint has changed. (404)
|
||||
case MojangResponseCode.ERROR_NOT_FOUND:
|
||||
// Selecting profiles isn't implemented yet. (Shouldnt happen)
|
||||
case MojangResponseCode.ERROR_ACCESS_TOKEN_HAS_PROFILE:
|
||||
// Username/password was not submitted. (UI should forbid this)
|
||||
case MojangResponseCode.ERROR_CREDENTIALS_ARE_NULL:
|
||||
// ??? (Shouldnt happen)
|
||||
case MojangResponseCode.ERROR_INVALID_SALT_VERSION:
|
||||
// Data was not submitted as application/json
|
||||
case MojangResponseCode.ERROR_UNSUPPORTED_MEDIA_TYPE:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
@ -1,7 +1,14 @@
|
||||
export enum StatusColor {
|
||||
RED = 'red',
|
||||
YELLOW = 'yellow',
|
||||
GREEN = 'green',
|
||||
GREY = 'grey'
|
||||
}
|
||||
|
||||
export interface Status {
|
||||
|
||||
service: string
|
||||
status: 'red' | 'yellow' | 'green' | 'grey'
|
||||
status: StatusColor
|
||||
name: string
|
||||
essential: boolean
|
||||
|
||||
|
177
test/mojang/mojangTest.ts
Normal file
177
test/mojang/mojangTest.ts
Normal file
@ -0,0 +1,177 @@
|
||||
import { Mojang } from "../../src/main/mojang/mojang"
|
||||
import { expect } from 'chai'
|
||||
import nock from 'nock'
|
||||
import { URL } from 'url'
|
||||
import { Session } from "../../src/main/model/mojang/auth/Session"
|
||||
import { MojangResponseCode } from "../../src/main/mojang/type/Response"
|
||||
|
||||
function expectMojangResponse(res: any, responseCode: MojangResponseCode, negate = false) {
|
||||
expect(res).to.not.be.an('error')
|
||||
expect(res).to.be.an('object')
|
||||
expect(res).to.have.property('responseCode')
|
||||
if(!negate) {
|
||||
expect(res.responseCode).to.equal(responseCode)
|
||||
} else {
|
||||
expect(res.responseCode).to.not.equal(responseCode)
|
||||
}
|
||||
}
|
||||
|
||||
describe('Mojang Errors', () => {
|
||||
|
||||
it('Status (Offline)', async () => {
|
||||
|
||||
const defStatusHack = Mojang['statuses']
|
||||
const url = new URL(Mojang.STATUS_ENDPOINT)
|
||||
|
||||
nock(url.origin)
|
||||
.get(url.pathname)
|
||||
.reply(500, 'Service temprarily offline.')
|
||||
|
||||
const res = await Mojang.status();
|
||||
expectMojangResponse(res, MojangResponseCode.SUCCESS, true)
|
||||
expect(res.data).to.be.an('array')
|
||||
expect(res.data).to.deep.equal(defStatusHack)
|
||||
|
||||
}).timeout(2500)
|
||||
|
||||
it('Authenticate (Invalid Credentials)', async () => {
|
||||
|
||||
nock(Mojang.AUTH_ENDPOINT)
|
||||
.post('/authenticate')
|
||||
.reply(403, (uri, requestBody: any): { error: string, errorMessage: string } => {
|
||||
return {
|
||||
error: 'ForbiddenOperationException',
|
||||
errorMessage: 'Invalid credentials. Invalid username or password.'
|
||||
}
|
||||
})
|
||||
|
||||
const res = await Mojang.authenticate('user', 'pass', 'xxx', true)
|
||||
expectMojangResponse(res, MojangResponseCode.ERROR_INVALID_CREDENTIALS)
|
||||
expect(res.data).to.be.a('null')
|
||||
expect(res.error).to.not.be.a('null')
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
describe('Mojang Status', () => {
|
||||
|
||||
it('Status (Online)', async () => {
|
||||
|
||||
const defStatusHack = Mojang['statuses']
|
||||
const url = new URL(Mojang.STATUS_ENDPOINT)
|
||||
|
||||
nock(url.origin)
|
||||
.get(url.pathname)
|
||||
.reply(200, defStatusHack)
|
||||
|
||||
const res = await Mojang.status();
|
||||
expectMojangResponse(res, MojangResponseCode.SUCCESS)
|
||||
expect(res.data).to.be.an('array')
|
||||
expect(res.data).to.deep.equal(defStatusHack)
|
||||
|
||||
}).timeout(2500)
|
||||
|
||||
})
|
||||
|
||||
describe('Mojang Auth', () => {
|
||||
|
||||
it('Authenticate', async () => {
|
||||
|
||||
nock(Mojang.AUTH_ENDPOINT)
|
||||
.post('/authenticate')
|
||||
.reply(200, (uri, requestBody: any): Session => {
|
||||
const mockResponse: Session = {
|
||||
accessToken: 'abc',
|
||||
clientToken: requestBody.clientToken,
|
||||
selectedProfile: {
|
||||
id: 'def',
|
||||
name: 'username'
|
||||
}
|
||||
}
|
||||
|
||||
if(requestBody.requestUser) {
|
||||
mockResponse.user = {
|
||||
id: 'def',
|
||||
properties: []
|
||||
}
|
||||
}
|
||||
|
||||
return mockResponse
|
||||
})
|
||||
|
||||
const res = await Mojang.authenticate('user', 'pass', 'xxx', true)
|
||||
expectMojangResponse(res, MojangResponseCode.SUCCESS)
|
||||
expect(res.data!.clientToken).to.equal('xxx')
|
||||
expect(res.data).to.have.property('user')
|
||||
|
||||
})
|
||||
|
||||
it('Validate', async () => {
|
||||
|
||||
nock(Mojang.AUTH_ENDPOINT)
|
||||
.post('/validate')
|
||||
.times(2)
|
||||
.reply((uri, requestBody: any) => {
|
||||
return [
|
||||
requestBody.accessToken === 'abc' ? 204 : 403
|
||||
]
|
||||
})
|
||||
|
||||
const res = await Mojang.validate('abc', 'def')
|
||||
|
||||
expectMojangResponse(res, MojangResponseCode.SUCCESS)
|
||||
expect(res.data).to.be.a('boolean')
|
||||
expect(res.data).to.equal(true)
|
||||
|
||||
const res2 = await Mojang.validate('def', 'def')
|
||||
|
||||
expectMojangResponse(res2, MojangResponseCode.SUCCESS)
|
||||
expect(res2.data).to.be.a('boolean')
|
||||
expect(res2.data).to.equal(false)
|
||||
|
||||
})
|
||||
|
||||
it('Invalidate', async () => {
|
||||
|
||||
nock(Mojang.AUTH_ENDPOINT)
|
||||
.post('/invalidate')
|
||||
.reply(204)
|
||||
|
||||
const res = await Mojang.invalidate('adc', 'def')
|
||||
|
||||
expectMojangResponse(res, MojangResponseCode.SUCCESS)
|
||||
|
||||
})
|
||||
|
||||
it('Refresh', async () => {
|
||||
|
||||
nock(Mojang.AUTH_ENDPOINT)
|
||||
.post('/refresh')
|
||||
.reply(200, (uri, requestBody: any): Session => {
|
||||
const mockResponse: Session = {
|
||||
accessToken: 'abc',
|
||||
clientToken: requestBody.clientToken,
|
||||
selectedProfile: {
|
||||
id: 'def',
|
||||
name: 'username'
|
||||
}
|
||||
}
|
||||
|
||||
if(requestBody.requestUser) {
|
||||
mockResponse.user = {
|
||||
id: 'def',
|
||||
properties: []
|
||||
}
|
||||
}
|
||||
|
||||
return mockResponse
|
||||
})
|
||||
|
||||
const res = await Mojang.refresh('gfd', 'xxx', true)
|
||||
expectMojangResponse(res, MojangResponseCode.SUCCESS)
|
||||
expect(res.data!.clientToken).to.equal('xxx')
|
||||
expect(res.data).to.have.property('user')
|
||||
|
||||
})
|
||||
|
||||
})
|
7
tsconfig.test.json
Normal file
7
tsconfig.test.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"extends": "./tsconfig.json"
|
||||
}
|
Loading…
Reference in New Issue
Block a user