Bladeren bron

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)
Daniel Scalzi 5 jaren geleden
bovenliggende
commit
9097bafb5d
8 gewijzigde bestanden met toevoegingen van 1194 en 297 verwijderingen
  1. 691 148
      package-lock.json
  2. 17 5
      package.json
  3. 40 0
      src/main/logging/loggerutil.ts
  4. 167 143
      src/main/mojang/mojang.ts
  5. 87 0
      src/main/mojang/type/Response.ts
  6. 8 1
      src/main/mojang/type/Status.ts
  7. 177 0
      test/mojang/mojangTest.ts
  8. 7 0
      tsconfig.test.json

File diff suppressed because it is too large
+ 691 - 148
package-lock.json


+ 17 - 5
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 - 0
src/main/logging/loggerutil.ts

@@ -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()
+            ]
+        })
+    }
+
+}

+ 167 - 143
src/main/mojang/mojang.ts

@@ -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'}[]){
-
-                    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)
+    public static async status(): Promise<MojangResponse<Status[]>>{
+        try {
+
+            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 - 0
src/main/mojang/type/Response.ts

@@ -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
+    }
+}

+ 8 - 1
src/main/mojang/type/Status.ts

@@ -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 - 0
test/mojang/mojangTest.ts

@@ -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 - 0
tsconfig.test.json

@@ -0,0 +1,7 @@
+{
+  "compilerOptions": {
+    "module": "commonjs",
+    "esModuleInterop": true
+  },
+  "extends": "./tsconfig.json"
+}

Some files were not shown because too many files changed in this diff