Pull out common got error handling for generic use. Initial distribution loading (no application state storage yet).

This commit is contained in:
Daniel Scalzi 2020-08-24 20:18:08 -04:00
parent 15fd2c842a
commit bc43d842e3
No known key found for this signature in database
GPG Key ID: D18EA3FB4B142A57
12 changed files with 629 additions and 520 deletions

654
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -31,13 +31,13 @@
"dependencies": { "dependencies": {
"adm-zip": "^0.4.16", "adm-zip": "^0.4.16",
"async": "^3.2.0", "async": "^3.2.0",
"discord-rpc": "^3.1.1", "discord-rpc": "^3.1.3",
"electron-updater": "^4.3.1", "electron-updater": "^4.3.4",
"fs-extra": "^9.0.1", "fs-extra": "^9.0.1",
"github-syntax-dark": "^0.5.0", "github-syntax-dark": "^0.5.0",
"got": "^11.5.0", "got": "^11.5.2",
"jquery": "^3.5.1", "jquery": "^3.5.1",
"lodash": "^4.17.19", "lodash": "^4.17.20",
"moment": "^2.27.0", "moment": "^2.27.0",
"request": "^2.88.2", "request": "^2.88.2",
"semver": "^7.3.2", "semver": "^7.3.2",
@ -50,46 +50,46 @@
"@babel/preset-react": "^7.10.4", "@babel/preset-react": "^7.10.4",
"@types/adm-zip": "^0.4.33", "@types/adm-zip": "^0.4.33",
"@types/async": "^3.2.3", "@types/async": "^3.2.3",
"@types/chai": "^4.2.11", "@types/chai": "^4.2.12",
"@types/discord-rpc": "^3.0.4", "@types/discord-rpc": "^3.0.4",
"@types/fs-extra": "^9.0.1", "@types/fs-extra": "^9.0.1",
"@types/jquery": "^3.5.0", "@types/jquery": "^3.5.1",
"@types/lodash": "^4.14.157", "@types/lodash": "^4.14.160",
"@types/mocha": "^8.0.0", "@types/mocha": "^8.0.3",
"@types/node": "^12.12.50", "@types/node": "^12.12.54",
"@types/react": "^16.9.43", "@types/react": "^16.9.46",
"@types/react-dom": "^16.9.8", "@types/react-dom": "^16.9.8",
"@types/react-redux": "^7.1.9", "@types/react-redux": "^7.1.9",
"@types/react-transition-group": "^4.4.0", "@types/react-transition-group": "^4.4.0",
"@types/request": "^2.48.5", "@types/request": "^2.48.5",
"@types/tar-fs": "^2.0.0", "@types/tar-fs": "^2.0.0",
"@types/triple-beam": "^1.3.1", "@types/triple-beam": "^1.3.2",
"@types/winreg": "^1.2.30", "@types/winreg": "^1.2.30",
"@typescript-eslint/eslint-plugin": "^3.6.1", "@typescript-eslint/eslint-plugin": "^3.10.0",
"@typescript-eslint/parser": "^3.6.1", "@typescript-eslint/parser": "^3.10.0",
"chai": "^4.2.0", "chai": "^4.2.0",
"cross-env": "^7.0.2", "cross-env": "^7.0.2",
"electron": "^9.1.0", "electron": "^9.2.1",
"electron-builder": "^22.7.0", "electron-builder": "^22.8.0",
"electron-devtools-installer": "^3.1.0", "electron-devtools-installer": "^3.1.1",
"electron-webpack": "^2.8.2", "electron-webpack": "^2.8.2",
"electron-webpack-ts": "^4.0.1", "electron-webpack-ts": "^4.0.1",
"eslint": "^7.4.0", "eslint": "^7.7.0",
"eslint-plugin-react": "^7.20.3", "eslint-plugin-react": "^7.20.6",
"helios-distribution-types": "1.0.0-pre.1", "helios-distribution-types": "1.0.0-pre.1",
"mocha": "^8.0.1", "mocha": "^8.1.1",
"nock": "^13.0.2", "nock": "^13.0.4",
"react": "^16.13.0", "react": "^16.13.0",
"react-dom": "^16.13.0", "react-dom": "^16.13.0",
"react-hot-loader": "^4.12.21", "react-hot-loader": "^4.12.21",
"react-redux": "^7.2.0", "react-redux": "^7.2.1",
"react-transition-group": "^4.4.1", "react-transition-group": "^4.4.1",
"redux": "^4.0.5", "redux": "^4.0.5",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"ts-node": "^8.10.2", "ts-node": "^8.10.2",
"tsconfig-paths": "^3.9.0", "tsconfig-paths": "^3.9.0",
"typescript": "^3.9.6", "typescript": "^3.9.7",
"webpack": "^4.43.0" "webpack": "^4.44.1"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -0,0 +1,106 @@
import { resolve } from 'path'
import { Distribution } from 'helios-distribution-types'
import got from 'got'
import { LoggerUtil } from 'common/logging/loggerutil'
import { RestResponse, handleGotError, RestResponseStatus } from 'common/got/RestResponse'
import { pathExists, readFile, writeFile } from 'fs-extra'
export class DistributionAPI {
private static readonly logger = LoggerUtil.getLogger('DistributionAPI')
private readonly REMOTE_URL = 'http://mc.westeroscraft.com/WesterosCraftLauncher/distribution.json'
private readonly DISTRO_FILE = 'distribution.json'
private readonly DISTRO_FILE_DEV = 'distribution_dev.json'
private readonly DEV_MODE = false // placeholder
private distroPath: string
private distroDevPath: string
private rawDistribution!: Distribution
constructor(
private launcherDirectory: string
) {
this.distroPath = resolve(launcherDirectory, this.DISTRO_FILE)
this.distroDevPath = resolve(launcherDirectory, this.DISTRO_FILE_DEV)
}
public async testLoad(): Promise<Distribution> {
await this.loadDistribution()
return this.rawDistribution
}
protected async loadDistribution(): Promise<void> {
let distro
if(!this.DEV_MODE) {
distro = (await this.pullRemote()).data
if(distro == null) {
distro = await this.pullLocal(false)
} else {
this.writeDistributionToDisk(distro)
}
} else {
distro = await this.pullLocal(true)
}
if(distro == null) {
// TODO Bubble this up nicer
throw new Error('FATAL: Unable to load distribution from remote server or local disk.')
}
this.rawDistribution = distro
}
protected async pullRemote(): Promise<RestResponse<Distribution | null>> {
try {
const res = await got.get<Distribution>(this.REMOTE_URL, { responseType: 'json' })
return {
data: res.body,
responseStatus: RestResponseStatus.SUCCESS
}
} catch(error) {
return handleGotError('Pull Remote', error, DistributionAPI.logger, () => null)
}
}
protected async writeDistributionToDisk(distribution: Distribution): Promise<void> {
await writeFile(this.distroPath, distribution)
}
protected async pullLocal(dev: boolean): Promise<Distribution | null> {
return await this.readDistributionFromFile(!dev ? this.distroPath : this.distroDevPath)
}
protected async readDistributionFromFile(path: string): Promise<Distribution | null> {
if(await pathExists(path)) {
const raw = await readFile(path, 'utf-8')
try {
return JSON.parse(raw)
} catch(error) {
DistributionAPI.logger.error(`Malformed distribution file at ${path}`)
return null
}
} else {
DistributionAPI.logger.error(`No distribution file found at ${path}!`)
return null
}
}
}

View File

@ -0,0 +1,43 @@
import { RequestError, HTTPError, TimeoutError, ParseError } from 'got'
import { Logger } from 'winston'
export enum RestResponseStatus {
SUCCESS,
ERROR
}
export interface RestResponse<T> {
data: T
responseStatus: RestResponseStatus
error?: RequestError
}
export function handleGotError<T>(operation: string, error: RequestError, logger: Logger, dataProvider: () => T): RestResponse<T> {
const response: RestResponse<T> = {
data: dataProvider(),
responseStatus: RestResponseStatus.ERROR,
error
}
if(error instanceof HTTPError) {
logger.error(`Error during ${operation} request (HTTP Response ${error.response.statusCode})`, error)
logger.debug('Response Details:')
logger.debug('Body:', error.response.body)
logger.debug('Headers:', error.response.headers)
} else if(Object.getPrototypeOf(error) instanceof RequestError) {
logger.error(`${operation} request recieved no response (${error.code}).`, error)
} else if(error instanceof TimeoutError) {
logger.error(`${operation} request timed out (${error.timings.phases.total}ms).`)
} else if(error instanceof ParseError) {
logger.error(`${operation} request recieved unexepected body (Parse Error).`)
} else {
// CacheError, ReadError, MaxRedirectsError, UnsupportedProtocolError, CancelError
logger.error(`Error during ${operation} request.`, error)
}
return response
}

View File

@ -1,8 +1,12 @@
import { createLogger, format, transports, Logger } from 'winston' import { createLogger, format, transports, Logger } from 'winston'
import { SPLAT } from 'triple-beam' import { SPLAT as SPLAT_Symbol } from 'triple-beam'
import moment from 'moment' import moment from 'moment'
import { inspect } from 'util' import { inspect } from 'util'
// Workaround until fixed.
// https://github.com/winstonjs/logform/issues/111
const SPLAT = SPLAT_Symbol as unknown as string
export class LoggerUtil { export class LoggerUtil {
public static getLogger(label: string): Logger { public static getLogger(label: string): Logger {

View File

@ -0,0 +1,87 @@
import { RestResponse } from 'common/got/RestResponse'
/**
* @see https://wiki.vg/Authentication#Errors
*/
export enum MojangErrorCode {
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
UNKNOWN
}
export interface MojangResponse<T> extends RestResponse<T> {
mojangErrorCode?: MojangErrorCode
isInternalError?: boolean
}
export interface MojangErrorBody {
error: string
errorMessage: string
cause?: string
}
/**
* Resolve the error response code from the response body.
*
* @param body The mojang error body response.
*/
export function decipherErrorCode(body: MojangErrorBody): MojangErrorCode {
if(body.error === 'Method Not Allowed') {
return MojangErrorCode.ERROR_METHOD_NOT_ALLOWED
} else if(body.error === 'Not Found') {
return MojangErrorCode.ERROR_NOT_FOUND
} else if(body.error === 'Unsupported Media Type') {
return MojangErrorCode.ERROR_UNSUPPORTED_MEDIA_TYPE
} else if(body.error === 'ForbiddenOperationException') {
if(body.cause && body.cause === 'UserMigratedException') {
return MojangErrorCode.ERROR_USER_MIGRATED
}
if(body.errorMessage === 'Invalid credentials. Invalid username or password.') {
return MojangErrorCode.ERROR_INVALID_CREDENTIALS
} else if(body.errorMessage === 'Invalid credentials.') {
return MojangErrorCode.ERROR_RATELIMIT
} else if(body.errorMessage === 'Invalid token.') {
return MojangErrorCode.ERROR_INVALID_TOKEN
}
} else if(body.error === 'IllegalArgumentException') {
if(body.errorMessage === 'Access token already has a profile assigned.') {
return MojangErrorCode.ERROR_ACCESS_TOKEN_HAS_PROFILE
} else if(body.errorMessage === 'credentials is null') {
return MojangErrorCode.ERROR_CREDENTIALS_ARE_NULL
} else if(body.errorMessage === 'Invalid salt version') {
return MojangErrorCode.ERROR_INVALID_SALT_VERSION
}
}
return MojangErrorCode.UNKNOWN
}
// These indicate problems with the code and not the data.
export function isInternalError(errorCode: MojangErrorCode): boolean {
switch(errorCode) {
case MojangErrorCode.ERROR_METHOD_NOT_ALLOWED: // We've sent the wrong method to an endpoint. (ex. GET to POST)
case MojangErrorCode.ERROR_NOT_FOUND: // Indicates endpoint has changed. (404)
case MojangErrorCode.ERROR_ACCESS_TOKEN_HAS_PROFILE: // Selecting profiles isn't implemented yet. (Shouldnt happen)
case MojangErrorCode.ERROR_CREDENTIALS_ARE_NULL: // Username/password was not submitted. (UI should forbid this)
case MojangErrorCode.ERROR_INVALID_SALT_VERSION: // ??? (Shouldnt happen)
case MojangErrorCode.ERROR_UNSUPPORTED_MEDIA_TYPE: // Data was not submitted as application/json
return true
default:
return false
}
}

View File

@ -1,87 +0,0 @@
import { RequestError } from 'got'
/**
* @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?: RequestError
isInternalError?: boolean
}
export interface MojangErrorBody {
error: string
errorMessage: string
cause?: string
}
export function deciperResponseCode(body: MojangErrorBody): 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) {
case MojangResponseCode.ERROR_METHOD_NOT_ALLOWED: // We've sent the wrong method to an endpoint. (ex. GET to POST)
case MojangResponseCode.ERROR_NOT_FOUND: // Indicates endpoint has changed. (404)
case MojangResponseCode.ERROR_ACCESS_TOKEN_HAS_PROFILE: // Selecting profiles isn't implemented yet. (Shouldnt happen)
case MojangResponseCode.ERROR_CREDENTIALS_ARE_NULL: // Username/password was not submitted. (UI should forbid this)
case MojangResponseCode.ERROR_INVALID_SALT_VERSION: // ??? (Shouldnt happen)
case MojangResponseCode.ERROR_UNSUPPORTED_MEDIA_TYPE: // Data was not submitted as application/json
return true
default:
return false
}
}

View File

@ -1,10 +1,11 @@
import { LoggerUtil } from '../logging/loggerutil' import { LoggerUtil } from '../logging/loggerutil'
import { Agent } from './model/auth/Agent' import { Agent } from './model/auth/Agent'
import { Status, StatusColor } from './model/internal/Status' import { Status, StatusColor } from './model/internal/Status'
import got, { RequestError, HTTPError, TimeoutError, ParseError } from 'got' import got, { RequestError, HTTPError } from 'got'
import { Session } from './model/auth/Session' import { Session } from './model/auth/Session'
import { AuthPayload } from './model/auth/AuthPayload' import { AuthPayload } from './model/auth/AuthPayload'
import { MojangResponse, MojangResponseCode, deciperResponseCode, isInternalError, MojangErrorBody } from './model/internal/Response' import { MojangResponse, MojangErrorCode, decipherErrorCode, isInternalError, MojangErrorBody } from './model/internal/MojangResponse'
import { RestResponseStatus, handleGotError } from 'common/got/RestResponse'
export class Mojang { export class Mojang {
@ -90,31 +91,16 @@ export class Mojang {
} }
private static handleGotError<T>(operation: string, error: RequestError, dataProvider: () => T): MojangResponse<T> { private static handleGotError<T>(operation: string, error: RequestError, dataProvider: () => T): MojangResponse<T> {
const response: MojangResponse<T> = {
data: dataProvider(), const response: MojangResponse<T> = handleGotError(operation, error, Mojang.logger, dataProvider)
responseCode: MojangResponseCode.ERROR,
error
}
if(error instanceof HTTPError) { if(error instanceof HTTPError) {
response.responseCode = deciperResponseCode(error.response.body as MojangErrorBody) response.mojangErrorCode = decipherErrorCode(error.response.body as MojangErrorBody)
Mojang.logger.error(`Error during ${operation} request (HTTP Response ${error.response.statusCode})`, error)
Mojang.logger.debug('Response Details:')
Mojang.logger.debug('Body:', error.response.body)
Mojang.logger.debug('Headers:', error.response.headers)
} else if(Object.getPrototypeOf(error) instanceof RequestError) {
Mojang.logger.error(`${operation} request recieved no response (${error.code}).`, error)
} else if(error instanceof TimeoutError) {
Mojang.logger.error(`${operation} request timed out (${error.timings.phases.total}ms).`)
} else if(error instanceof ParseError) {
Mojang.logger.error(`${operation} request recieved unexepected body (Parse Error).`)
} else { } else {
// CacheError, ReadError, MaxRedirectsError, UnsupportedProtocolError, CancelError response.mojangErrorCode = MojangErrorCode.UNKNOWN
Mojang.logger.error(`Error during ${operation} request.`, error)
} }
response.isInternalError = isInternalError(response.mojangErrorCode)
response.isInternalError = isInternalError(response.responseCode)
return response return response
} }
@ -151,7 +137,7 @@ export class Mojang {
return { return {
data: Mojang.statuses, data: Mojang.statuses,
responseCode: MojangResponseCode.SUCCESS responseStatus: RestResponseStatus.SUCCESS
} }
} catch(error) { } catch(error) {
@ -201,7 +187,7 @@ export class Mojang {
Mojang.expectSpecificSuccess('Mojang Authenticate', 200, res.statusCode) Mojang.expectSpecificSuccess('Mojang Authenticate', 200, res.statusCode)
return { return {
data: res.body, data: res.body,
responseCode: MojangResponseCode.SUCCESS responseStatus: RestResponseStatus.SUCCESS
} }
} catch(err) { } catch(err) {
@ -233,14 +219,14 @@ export class Mojang {
return { return {
data: res.statusCode === 204, data: res.statusCode === 204,
responseCode: MojangResponseCode.SUCCESS responseStatus: RestResponseStatus.SUCCESS
} }
} catch(err) { } catch(err) {
if(err instanceof HTTPError && err.response.statusCode === 403) { if(err instanceof HTTPError && err.response.statusCode === 403) {
return { return {
data: false, data: false,
responseCode: MojangResponseCode.SUCCESS responseStatus: RestResponseStatus.SUCCESS
} }
} }
return Mojang.handleGotError('Mojang Validate', err, () => false) return Mojang.handleGotError('Mojang Validate', err, () => false)
@ -271,7 +257,7 @@ export class Mojang {
return { return {
data: undefined, data: undefined,
responseCode: MojangResponseCode.SUCCESS responseStatus: RestResponseStatus.SUCCESS
} }
} catch(err) { } catch(err) {
@ -306,7 +292,7 @@ export class Mojang {
return { return {
data: res.body, data: res.body,
responseCode: MojangResponseCode.SUCCESS responseStatus: RestResponseStatus.SUCCESS
} }
} catch(err) { } catch(err) {

View File

@ -113,7 +113,9 @@ async function createWindow() {
webPreferences: { webPreferences: {
preload: join(__dirname, '..', 'out', 'preloader.js'), preload: join(__dirname, '..', 'out', 'preloader.js'),
nodeIntegration: true, nodeIntegration: true,
contextIsolation: false contextIsolation: false,
enableRemoteModule: true,
worldSafeExecuteJavaScript: true
}, },
backgroundColor: '#171614' backgroundColor: '#171614'
}) })

View File

@ -17,6 +17,8 @@ import { join } from 'path'
import Overlay from './overlay/Overlay' import Overlay from './overlay/Overlay'
import { OverlayPushAction, OverlayActionDispatch } from '../redux/actions/overlayActions' import { OverlayPushAction, OverlayActionDispatch } from '../redux/actions/overlayActions'
import { DistributionAPI } from 'common/distribution/distribution'
import './Application.css' import './Application.css'
declare const __static: string declare const __static: string
@ -120,9 +122,14 @@ class Application extends React.Component<ApplicationProps & typeof mapDispatch,
setTimeout(() => { setTimeout(() => {
//this.props.setView(View.WELCOME) //this.props.setView(View.WELCOME)
this.props.pushGenericOverlay({ this.props.pushGenericOverlay({
title: 'Test Title', title: 'Load Distribution',
description: 'Test Description', description: 'This is a test. Will load the distribution.',
dismissible: true dismissible: false,
acknowledgeCallback: async () => {
const distro = new DistributionAPI('C:\\Users\\user\\AppData\\Roaming\\Helios Launcher')
const x = await distro.testLoad()
console.log(x)
}
}) })
this.props.pushGenericOverlay({ this.props.pushGenericOverlay({
title: 'Test Title 2', title: 'Test Title 2',

View File

@ -10,8 +10,8 @@ export interface GenericOverlayProps {
acknowledgeText?: string acknowledgeText?: string
dismissText?: string dismissText?: string
dismissible: boolean dismissible: boolean
acknowledgeCallback?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void acknowledgeCallback?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => Promise<void>
dismissCallback?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void dismissCallback?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => Promise<void>
} }
const mapDispatch = { const mapDispatch = {
@ -30,16 +30,16 @@ class GenericOverlay extends React.Component<InternalGenericOverlayProps> {
return this.props.dismissText || 'Dismiss' return this.props.dismissText || 'Dismiss'
} }
private onAcknowledgeClick = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>): void => { private onAcknowledgeClick = async (event: React.MouseEvent<HTMLButtonElement, MouseEvent>): Promise<void> => {
if(this.props.acknowledgeCallback) { if(this.props.acknowledgeCallback) {
this.props.acknowledgeCallback(event) await this.props.acknowledgeCallback(event)
} }
this.props.popOverlayContent() this.props.popOverlayContent()
} }
private onDismissClick = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>): void => { private onDismissClick = async (event: React.MouseEvent<HTMLButtonElement, MouseEvent>): Promise<void> => {
if(this.props.dismissCallback) { if(this.props.dismissCallback) {
this.props.dismissCallback(event) await this.props.dismissCallback(event)
} }
this.props.popOverlayContent() this.props.popOverlayContent()
} }

View File

@ -1,17 +1,33 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Mojang } from 'common/mojang/mojang' import { Mojang } from 'common/mojang/mojang'
import { expect } from 'chai' import { expect } from 'chai'
import nock from 'nock' import nock from 'nock'
import { Session } from 'common/mojang/model/auth/Session' import { Session } from 'common/mojang/model/auth/Session'
import { MojangResponseCode } from 'common/mojang/model/internal/Response' import { MojangErrorCode, MojangResponse } from 'common/mojang/model/internal/MojangResponse'
import { RestResponseStatus, RestResponse } from 'common/got/RestResponse'
function expectMojangResponse(res: any, responseCode: MojangResponseCode, negate = false) { function assertResponse(res: RestResponse<unknown>) {
expect(res).to.not.be.an('error') expect(res).to.not.be.an('error')
expect(res).to.be.an('object') expect(res).to.be.an('object')
expect(res).to.have.property('responseCode') }
function expectSuccess(res: RestResponse<unknown>) {
assertResponse(res)
expect(res).to.have.property('responseStatus')
expect(res.responseStatus).to.equal(RestResponseStatus.SUCCESS)
}
function expectFailure(res: RestResponse<unknown>) {
expect(res.responseStatus).to.not.equal(RestResponseStatus.SUCCESS)
}
function expectMojangResponse(res: MojangResponse<unknown>, responseCode: MojangErrorCode, negate = false) {
assertResponse(res)
expect(res).to.have.property('mojangErrorCode')
if(!negate) { if(!negate) {
expect(res.responseCode).to.equal(responseCode) expect(res.mojangErrorCode).to.equal(responseCode)
} else { } else {
expect(res.responseCode).to.not.equal(responseCode) expect(res.mojangErrorCode).to.not.equal(responseCode)
} }
} }
@ -30,7 +46,7 @@ describe('Mojang Errors', () => {
.reply(500, 'Service temprarily offline.') .reply(500, 'Service temprarily offline.')
const res = await Mojang.status() const res = await Mojang.status()
expectMojangResponse(res, MojangResponseCode.SUCCESS, true) expectFailure(res)
expect(res.data).to.be.an('array') expect(res.data).to.be.an('array')
expect(res.data).to.deep.equal(defStatusHack) expect(res.data).to.deep.equal(defStatusHack)
@ -40,7 +56,8 @@ describe('Mojang Errors', () => {
nock(Mojang.AUTH_ENDPOINT) nock(Mojang.AUTH_ENDPOINT)
.post('/authenticate') .post('/authenticate')
.reply(403, (uri, requestBody: any): { error: string, errorMessage: string } => { // eslint-disable-next-line @typescript-eslint/no-unused-vars
.reply(403, (uri, requestBody: unknown): { error: string, errorMessage: string } => {
return { return {
error: 'ForbiddenOperationException', error: 'ForbiddenOperationException',
errorMessage: 'Invalid credentials. Invalid username or password.' errorMessage: 'Invalid credentials. Invalid username or password.'
@ -48,7 +65,7 @@ describe('Mojang Errors', () => {
}) })
const res = await Mojang.authenticate('user', 'pass', 'xxx', true) const res = await Mojang.authenticate('user', 'pass', 'xxx', true)
expectMojangResponse(res, MojangResponseCode.ERROR_INVALID_CREDENTIALS) expectMojangResponse(res, MojangErrorCode.ERROR_INVALID_CREDENTIALS)
expect(res.data).to.be.a('null') expect(res.data).to.be.a('null')
expect(res.error).to.not.be.a('null') expect(res.error).to.not.be.a('null')
@ -66,7 +83,7 @@ describe('Mojang Status', () => {
.reply(200, defStatusHack) .reply(200, defStatusHack)
const res = await Mojang.status() const res = await Mojang.status()
expectMojangResponse(res, MojangResponseCode.SUCCESS) expectSuccess(res)
expect(res.data).to.be.an('array') expect(res.data).to.be.an('array')
expect(res.data).to.deep.equal(defStatusHack) expect(res.data).to.deep.equal(defStatusHack)
@ -101,7 +118,7 @@ describe('Mojang Auth', () => {
}) })
const res = await Mojang.authenticate('user', 'pass', 'xxx', true) const res = await Mojang.authenticate('user', 'pass', 'xxx', true)
expectMojangResponse(res, MojangResponseCode.SUCCESS) expectSuccess(res)
expect(res.data!.clientToken).to.equal('xxx') expect(res.data!.clientToken).to.equal('xxx')
expect(res.data).to.have.property('user') expect(res.data).to.have.property('user')
@ -120,13 +137,13 @@ describe('Mojang Auth', () => {
const res = await Mojang.validate('abc', 'def') const res = await Mojang.validate('abc', 'def')
expectMojangResponse(res, MojangResponseCode.SUCCESS) expectSuccess(res)
expect(res.data).to.be.a('boolean') expect(res.data).to.be.a('boolean')
expect(res.data).to.equal(true) expect(res.data).to.equal(true)
const res2 = await Mojang.validate('def', 'def') const res2 = await Mojang.validate('def', 'def')
expectMojangResponse(res2, MojangResponseCode.SUCCESS) expectSuccess(res2)
expect(res2.data).to.be.a('boolean') expect(res2.data).to.be.a('boolean')
expect(res2.data).to.equal(false) expect(res2.data).to.equal(false)
@ -140,7 +157,7 @@ describe('Mojang Auth', () => {
const res = await Mojang.invalidate('adc', 'def') const res = await Mojang.invalidate('adc', 'def')
expectMojangResponse(res, MojangResponseCode.SUCCESS) expectSuccess(res)
}) })
@ -169,7 +186,7 @@ describe('Mojang Auth', () => {
}) })
const res = await Mojang.refresh('gfd', 'xxx', true) const res = await Mojang.refresh('gfd', 'xxx', true)
expectMojangResponse(res, MojangResponseCode.SUCCESS) expectSuccess(res)
expect(res.data!.clientToken).to.equal('xxx') expect(res.data!.clientToken).to.equal('xxx')
expect(res.data).to.have.property('user') expect(res.data).to.have.property('user')