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:
Daniel Scalzi 2020-04-13 22:21:48 -04:00
parent 761a46060b
commit 9097bafb5d
No known key found for this signature in database
GPG Key ID: D18EA3FB4B142A57
8 changed files with 1338 additions and 168 deletions

856
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -22,7 +22,8 @@
"dist:linux": "npm run dist -- LINUX", "dist:linux": "npm run dist -- LINUX",
"lint": "eslint --ext=jsx,js,tsx,ts src", "lint": "eslint --ext=jsx,js,tsx,ts src",
"dev": "electron-webpack dev", "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": { "engines": {
"node": "12.x.x" "node": "12.x.x"
@ -30,29 +31,37 @@
"dependencies": { "dependencies": {
"adm-zip": "^0.4.14", "adm-zip": "^0.4.14",
"async": "^3.2.0", "async": "^3.2.0",
"axios": "^0.19.2",
"discord-rpc": "3.1.0", "discord-rpc": "3.1.0",
"electron-updater": "^4.2.4", "electron-updater": "^4.2.4",
"fs-extra": "^9.0.0", "fs-extra": "^9.0.0",
"github-syntax-dark": "^0.5.0", "github-syntax-dark": "^0.5.0",
"jquery": "^3.4.1", "jquery": "^3.5.0",
"moment": "^2.24.0",
"request": "^2.88.2", "request": "^2.88.2",
"semver": "^7.1.3", "semver": "^7.2.2",
"tar-fs": "^2.0.0", "tar-fs": "^2.0.0",
"winreg": "^1.2.4" "triple-beam": "^1.3.0",
"winreg": "^1.2.4",
"winston": "^3.2.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/preset-react": "^7.9.4", "@babel/preset-react": "^7.9.4",
"@types/adm-zip": "^0.4.32", "@types/adm-zip": "^0.4.33",
"@types/async": "^3.0.8", "@types/async": "^3.0.8",
"@types/chai": "^4.2.11",
"@types/discord-rpc": "^3.0.2", "@types/discord-rpc": "^3.0.2",
"@types/fs-extra": "^8.1.0", "@types/fs-extra": "^8.1.0",
"@types/jquery": "^3.3.33", "@types/jquery": "^3.3.33",
"@types/mocha": "^7.0.2",
"@types/node": "^12.12.29", "@types/node": "^12.12.29",
"@types/react": "^16.9.23", "@types/react": "^16.9.23",
"@types/react-dom": "^16.9.5", "@types/react-dom": "^16.9.5",
"@types/request": "^2.48.4", "@types/request": "^2.48.4",
"@types/tar-fs": "^1.16.2", "@types/tar-fs": "^1.16.2",
"@types/triple-beam": "^1.3.0",
"@types/winreg": "^1.2.30", "@types/winreg": "^1.2.30",
"chai": "^4.2.0",
"cross-env": "^7.0.2", "cross-env": "^7.0.2",
"electron": "^8.2.1", "electron": "^8.2.1",
"electron-builder": "^22.4.0", "electron-builder": "^22.4.0",
@ -60,10 +69,13 @@
"electron-webpack-ts": "^4.0.1", "electron-webpack-ts": "^4.0.1",
"eslint": "^6.8.0", "eslint": "^6.8.0",
"helios-distribution-types": "1.0.0-pre.1", "helios-distribution-types": "1.0.0-pre.1",
"mocha": "^7.1.1",
"nock": "^12.0.3",
"react": "^16.13.0", "react": "^16.13.0",
"react-dom": "^16.13.0", "react-dom": "^16.13.0",
"react-hot-loader": "^4.12.19", "react-hot-loader": "^4.12.19",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"ts-node": "^8.8.2",
"typescript": "^3.8.3", "typescript": "^3.8.3",
"webpack": "^4.42.0" "webpack": "^4.42.0"
}, },

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

View File

@ -1,15 +1,20 @@
import request from 'request' import { LoggerUtil } from '../logging/loggerutil'
import { LoggerUtil } from '../loggerutil'
import { Agent } from '../model/mojang/auth/Agent' 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 { 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 { 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 AUTH_ENDPOINT = 'https://authserver.mojang.com'
public static readonly STATUS_ENDPOINT = 'https://status.mojang.com/check'
public static readonly MINECRAFT_AGENT: Agent = { public static readonly MINECRAFT_AGENT: Agent = {
name: 'Minecraft', name: 'Minecraft',
version: 1 version: 1
@ -18,37 +23,37 @@ export class Mojang {
protected static statuses: Status[] = [ protected static statuses: Status[] = [
{ {
service: 'sessionserver.mojang.com', service: 'sessionserver.mojang.com',
status: 'grey', status: StatusColor.GREY,
name: 'Multiplayer Session Service', name: 'Multiplayer Session Service',
essential: true essential: true
}, },
{ {
service: 'authserver.mojang.com', service: 'authserver.mojang.com',
status: 'grey', status: StatusColor.GREY,
name: 'Authentication Service', name: 'Authentication Service',
essential: true essential: true
}, },
{ {
service: 'textures.minecraft.net', service: 'textures.minecraft.net',
status: 'grey', status: StatusColor.GREY,
name: 'Minecraft Skins', name: 'Minecraft Skins',
essential: false essential: false
}, },
{ {
service: 'api.mojang.com', service: 'api.mojang.com',
status: 'grey', status: StatusColor.GREY,
name: 'Public API', name: 'Public API',
essential: false essential: false
}, },
{ {
service: 'minecraft.net', service: 'minecraft.net',
status: 'grey', status: StatusColor.GREY,
name: 'Minecraft.net', name: 'Minecraft.net',
essential: false essential: false
}, },
{ {
service: 'account.mojang.com', service: 'account.mojang.com',
status: 'grey', status: StatusColor.GREY,
name: 'Mojang Accounts Website', name: 'Mojang Accounts Website',
essential: false essential: false
} }
@ -58,24 +63,50 @@ export class Mojang {
* Converts a Mojang status color to a hex value. Valid statuses * Converts a Mojang status color to a hex value. Valid statuses
* are 'green', 'yellow', 'red', and 'grey'. Grey is a custom status * are 'green', 'yellow', 'red', and 'grey'. Grey is a custom status
* to our project which represents an unknown 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){ public static statusToHex(status: string){
switch(status.toLowerCase()){ switch(status.toLowerCase()){
case 'green': case StatusColor.GREEN:
return '#a5c325' return '#a5c325'
case 'yellow': case StatusColor.YELLOW:
return '#eac918' return '#eac918'
case 'red': case StatusColor.RED:
return '#c32625' return '#c32625'
case 'grey': case StatusColor.GREY:
default: default:
return '#848484' 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. * Retrieves the status of Mojang's services.
* The response is condensed into a single object. Each service is * 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 * @see http://wiki.vg/Mojang_API#API_Status
*/ */
public static status(): Promise<Status[]>{ public static async status(): Promise<MojangResponse<Status[]>>{
return new Promise((resolve, reject) => { try {
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){ const res = await axios.get<{[service: string]: StatusColor}[]>(Mojang.STATUS_ENDPOINT, { timeout: Mojang.TIMEOUT })
Mojang.logger.warn('Unable to retrieve Mojang status.')
Mojang.logger.debug('Error while retrieving Mojang statuses:', error) Mojang.expectSpecificSuccess('Mojang Status', 200, res.status)
//reject(error || response.statusCode)
for(let i=0; i<Mojang.statuses.length; i++){ res.data.forEach(status => {
Mojang.statuses[i].status = 'grey' const entry = Object.entries(status)[0]
} for(let i=0; i<Mojang.statuses.length; i++) {
resolve(Mojang.statuses) if(Mojang.statuses[i].service === entry[0]) {
} else { Mojang.statuses[i].status = entry[1]
for(let i=0; i<body.length; i++){ break
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)
} }
}) }
}) })
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 * @see http://wiki.vg/Authentication#Authenticate
*/ */
public static authenticate( public static async authenticate(
username: string, username: string,
password: string, password: string,
clientToken: string | null, clientToken: string | null,
requestUser: boolean = true, requestUser: boolean = true,
agent: Agent = Mojang.MINECRAFT_AGENT agent: Agent = Mojang.MINECRAFT_AGENT
): Promise<Session> { ): Promise<MojangResponse<Session | null>> {
return new Promise((resolve, reject) => {
const body: AuthPayload = { try {
const data: AuthPayload = {
agent, agent,
username, username,
password, password,
requestUser requestUser
} }
if(clientToken != null){ if(clientToken != null){
body.clientToken = clientToken data.clientToken = clientToken
} }
request.post(Mojang.AUTH_ENDPOINT + '/authenticate', const res = await axios.post<Session>(`${Mojang.AUTH_ENDPOINT}/authenticate`, data)
{ Mojang.expectSpecificSuccess('Mojang Authenticate', 200, res.status)
json: true, return {
body data: res.data,
}, responseCode: MojangResponseCode.SUCCESS
function(error, response, body){ }
if(error){
Mojang.logger.error('Error during authentication.', error) } catch(err) {
reject(error) return Mojang.handleAxiosError('Mojang Authenticate', err, () => null)
} else { }
if(response.statusCode === 200){
resolve(body)
} else {
reject(body || {code: 'ENOTFOUND'})
}
}
})
})
} }
/** /**
@ -177,30 +202,33 @@ export class Mojang {
* *
* @see http://wiki.vg/Authentication#Validate * @see http://wiki.vg/Authentication#Validate
*/ */
public static validate(accessToken: string, clientToken: string): Promise<boolean> { public static async validate(accessToken: string, clientToken: string): Promise<MojangResponse<boolean>> {
return new Promise((resolve, reject) => {
request.post(Mojang.AUTH_ENDPOINT + '/validate', try {
{
json: true, const data = {
body: { accessToken,
accessToken, clientToken
clientToken }
}
}, const res = await axios.post(`${Mojang.AUTH_ENDPOINT}/validate`, data)
function(error, response, body){ Mojang.expectSpecificSuccess('Mojang Validate', 204, res.status)
if(error){
Mojang.logger.error('Error during validation.', error) return {
reject(error) data: res.status === 204,
} else { responseCode: MojangResponseCode.SUCCESS
if(response.statusCode === 403){ }
resolve(false)
} else { } catch(err) {
// 204 if valid if(err.response && err.response.status === 403) {
resolve(true) 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 * @see http://wiki.vg/Authentication#Invalidate
*/ */
public static invalidate(accessToken: string, clientToken: string): Promise<void>{ public static async invalidate(accessToken: string, clientToken: string): Promise<MojangResponse<undefined>> {
return new Promise((resolve, reject) => {
request.post(Mojang.AUTH_ENDPOINT + '/invalidate', try {
{
json: true, const data = {
body: { accessToken,
accessToken, clientToken
clientToken }
}
}, const res = await axios.post(`${Mojang.AUTH_ENDPOINT}/invalidate`, data)
function(error, response, body){ Mojang.expectSpecificSuccess('Mojang Invalidate', 204, res.status)
if(error){
Mojang.logger.error('Error during invalidation.', error) return {
reject(error) data: undefined,
} else { responseCode: MojangResponseCode.SUCCESS
if(response.statusCode === 204){ }
resolve()
} else { } catch(err) {
reject(body) return Mojang.handleAxiosError('Mojang Invalidate', err, () => undefined)
} }
}
})
})
} }
/** /**
@ -248,30 +274,28 @@ export class Mojang {
* *
* @see http://wiki.vg/Authentication#Refresh * @see http://wiki.vg/Authentication#Refresh
*/ */
public static refresh(accessToken: string, clientToken: string, requestUser: boolean = true): Promise<Session> { public static async refresh(accessToken: string, clientToken: string, requestUser: boolean = true): Promise<MojangResponse<Session | null>> {
return new Promise((resolve, reject) => {
request.post(Mojang.AUTH_ENDPOINT + '/refresh', try {
{
json: true, const data = {
body: { accessToken,
accessToken, clientToken,
clientToken, requestUser
requestUser }
}
}, const res = await axios.post<Session>(`${Mojang.AUTH_ENDPOINT}/refresh`, data)
function(error, response, body){ Mojang.expectSpecificSuccess('Mojang Refresh', 200, res.status)
if(error){
Mojang.logger.error('Error during refresh.', error) return {
reject(error) data: res.data,
} else { responseCode: MojangResponseCode.SUCCESS
if(response.statusCode === 200){ }
resolve(body)
} else { } catch(err) {
reject(body) return Mojang.handleAxiosError('Mojang Refresh', err, () => null)
} }
}
})
})
} }
} }

View 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
}
}

View File

@ -1,7 +1,14 @@
export enum StatusColor {
RED = 'red',
YELLOW = 'yellow',
GREEN = 'green',
GREY = 'grey'
}
export interface Status { export interface Status {
service: string service: string
status: 'red' | 'yellow' | 'green' | 'grey' status: StatusColor
name: string name: string
essential: boolean essential: boolean

177
test/mojang/mojangTest.ts Normal file
View 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
View File

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