Add Steam lifecycle logging and retry handling
This commit is contained in:
69
client.js
69
client.js
@@ -1,8 +1,8 @@
|
||||
const SteamUser = require('steam-user');
|
||||
const SteamCommunity = require('steamcommunity');
|
||||
const fs = require('fs');
|
||||
const winston = require("winston");
|
||||
const config = require("./config.js");
|
||||
const { createSteamLifecycle } = require('./steam-lifecycle');
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: 'info',
|
||||
@@ -18,65 +18,14 @@ const users = {};
|
||||
const steamUser = new SteamUser();
|
||||
const steamCommunity = new SteamCommunity();
|
||||
|
||||
steamUser.setOption("renewRefreshTokens", true);
|
||||
steamUser.on("refreshToken", (refreshToken) => {
|
||||
logger.info("Refresh token: " + refreshToken);
|
||||
fs.writeFileSync('refresh.token', refreshToken);
|
||||
});
|
||||
|
||||
try {
|
||||
const refreshToken = fs.readFileSync('refresh.token', 'utf8');
|
||||
|
||||
if (refreshToken && refreshToken.length > 0) {
|
||||
const LogOnOptionsAUTO = {
|
||||
logonID: config.logonID,
|
||||
refreshToken: refreshToken,
|
||||
steamID: config.steamID,
|
||||
};
|
||||
steamUser.logOn(LogOnOptionsAUTO);
|
||||
} else {
|
||||
steamUser.logOn({
|
||||
accountName: config.accountName,
|
||||
password: config.password,
|
||||
logonID: config.logonID,
|
||||
steamID: config.steamID,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn(`failed to load session: ${e.message}`)
|
||||
|
||||
steamUser.logOn({
|
||||
accountName: config.accountName,
|
||||
password: config.password,
|
||||
logonID: config.logonID,
|
||||
steamID: config.steamID,
|
||||
});
|
||||
}
|
||||
|
||||
const steamLoginPromise = new Promise((resolve, reject) => {
|
||||
steamUser.on('loggedOn', async () => {
|
||||
logger.info(`login to Steam as ${steamUser.steamID}`);
|
||||
try {
|
||||
steamUser.webLogOn();
|
||||
} catch (err) {
|
||||
logger.warn(`failed to start web login: ${err.message}`);
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
const steamWebLoginPromise = new Promise((resolve, reject) => {
|
||||
steamUser.on('webSession', async (sessionID, cookies) => {
|
||||
logger.info(`web session received: ${sessionID}`);
|
||||
|
||||
steamCommunity.setCookies(cookies);
|
||||
if (config.identitySecret) {
|
||||
steamCommunity.startConfirmationChecker(10000, config.identitySecret);
|
||||
}
|
||||
|
||||
resolve()
|
||||
});
|
||||
const {
|
||||
steamLoginPromise,
|
||||
steamWebLoginPromise,
|
||||
} = createSteamLifecycle({
|
||||
steamUser,
|
||||
steamCommunity,
|
||||
logger,
|
||||
config,
|
||||
});
|
||||
|
||||
async function getUserInfo(steamID, onUserInfoReceived) {
|
||||
|
||||
318
steam-lifecycle.js
Normal file
318
steam-lifecycle.js
Normal file
@@ -0,0 +1,318 @@
|
||||
const fs = require('fs');
|
||||
const EResult = require('steam-user/enums/EResult');
|
||||
|
||||
const DEFAULT_INITIAL_RETRY_DELAY_MS = 5000;
|
||||
const DEFAULT_MAX_RETRY_DELAY_MS = 5 * 60 * 1000;
|
||||
|
||||
const RECOVERABLE_ERESULTS = new Set([
|
||||
EResult.Invalid,
|
||||
EResult.Fail,
|
||||
EResult.NoConnection,
|
||||
EResult.Busy,
|
||||
EResult.Timeout,
|
||||
EResult.ServiceUnavailable,
|
||||
EResult.TryAnotherCM,
|
||||
]);
|
||||
|
||||
const NON_RECOVERABLE_ERESULTS = new Set([
|
||||
EResult.InvalidPassword,
|
||||
EResult.LoggedInElsewhere,
|
||||
EResult.Banned,
|
||||
EResult.AccountNotFound,
|
||||
EResult.InvalidSteamID,
|
||||
EResult.AccountDisabled,
|
||||
EResult.AlreadyLoggedInElsewhere,
|
||||
EResult.Suspended,
|
||||
EResult.PasswordUnset,
|
||||
EResult.IllegalPassword,
|
||||
EResult.AccountLogonDenied,
|
||||
EResult.InvalidLoginAuthCode,
|
||||
EResult.AccountLogonDeniedNoMail,
|
||||
EResult.ExpiredLoginAuthCode,
|
||||
EResult.IPLoginRestrictionFailed,
|
||||
EResult.AccountLockedDown,
|
||||
EResult.AccountLogonDeniedVerifiedEmailRequired,
|
||||
EResult.RequirePasswordReEntry,
|
||||
EResult.RateLimitExceeded,
|
||||
EResult.AccountLoginDeniedNeedTwoFactor,
|
||||
EResult.AccountLoginDeniedThrottle,
|
||||
EResult.TwoFactorCodeMismatch,
|
||||
EResult.TimeNotSynced,
|
||||
EResult.NeedCaptcha,
|
||||
EResult.IPBanned,
|
||||
EResult.LimitedUserAccount,
|
||||
].filter((value) => typeof value === 'number'));
|
||||
|
||||
const NON_RECOVERABLE_MESSAGE_PATTERNS = [
|
||||
/invalid password/i,
|
||||
/invalid refresh token/i,
|
||||
/refreshToken is not/i,
|
||||
/not valid for logging in/i,
|
||||
/does not match refreshToken/i,
|
||||
/steam guard/i,
|
||||
/two[- ]?factor/i,
|
||||
/account login denied/i,
|
||||
/logged in elsewhere/i,
|
||||
];
|
||||
|
||||
const RECOVERABLE_MESSAGE_PATTERNS = [
|
||||
/no steam servers available/i,
|
||||
/no connection/i,
|
||||
/service unavailable/i,
|
||||
/try another cm/i,
|
||||
/timeout/i,
|
||||
/timed out/i,
|
||||
/econnreset/i,
|
||||
/econnrefused/i,
|
||||
/enotfound/i,
|
||||
/eai_again/i,
|
||||
/socket/i,
|
||||
/tls/i,
|
||||
/network/i,
|
||||
/rate limit/i,
|
||||
];
|
||||
|
||||
function getEResultName(eresult) {
|
||||
if (eresult === undefined || eresult === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return EResult[eresult] || String(eresult);
|
||||
}
|
||||
|
||||
function buildLogOnOptions(config, fsModule = fs, refreshTokenPath = 'refresh.token') {
|
||||
try {
|
||||
const refreshToken = fsModule.readFileSync(refreshTokenPath, 'utf8').trim();
|
||||
|
||||
if (refreshToken.length > 0) {
|
||||
return {
|
||||
options: {
|
||||
logonID: config.logonID,
|
||||
refreshToken,
|
||||
steamID: config.steamID,
|
||||
},
|
||||
mode: 'refreshToken',
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
// Missing or unreadable refresh tokens are expected on first start.
|
||||
}
|
||||
|
||||
return {
|
||||
options: {
|
||||
accountName: config.accountName,
|
||||
password: config.password,
|
||||
logonID: config.logonID,
|
||||
steamID: config.steamID,
|
||||
},
|
||||
mode: 'credentials',
|
||||
};
|
||||
}
|
||||
|
||||
function isRecoverableSteamError(err) {
|
||||
if (!err) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const message = String(err.message || err);
|
||||
if (NON_RECOVERABLE_ERESULTS.has(err.eresult)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (NON_RECOVERABLE_MESSAGE_PATTERNS.some((pattern) => pattern.test(message))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (RECOVERABLE_ERESULTS.has(err.eresult)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (RECOVERABLE_MESSAGE_PATTERNS.some((pattern) => pattern.test(message))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function createSteamLifecycle({
|
||||
steamUser,
|
||||
steamCommunity,
|
||||
logger,
|
||||
config,
|
||||
fsModule = fs,
|
||||
setTimeoutFn = setTimeout,
|
||||
clearTimeoutFn = clearTimeout,
|
||||
refreshTokenPath = 'refresh.token',
|
||||
initialRetryDelayMs = DEFAULT_INITIAL_RETRY_DELAY_MS,
|
||||
maxRetryDelayMs = DEFAULT_MAX_RETRY_DELAY_MS,
|
||||
}) {
|
||||
let loginResolved = false;
|
||||
let loginRejected = false;
|
||||
let retryDelayMs = initialRetryDelayMs;
|
||||
let retryTimer = null;
|
||||
let confirmationCheckerStarted = false;
|
||||
|
||||
let resolveLogin;
|
||||
let rejectLogin;
|
||||
const steamLoginPromise = new Promise((resolve, reject) => {
|
||||
resolveLogin = resolve;
|
||||
rejectLogin = reject;
|
||||
});
|
||||
|
||||
let resolveWebLogin;
|
||||
const steamWebLoginPromise = new Promise((resolve) => {
|
||||
resolveWebLogin = resolve;
|
||||
});
|
||||
|
||||
function clearRetryTimer() {
|
||||
if (retryTimer) {
|
||||
clearTimeoutFn(retryTimer);
|
||||
retryTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function logOn(reason) {
|
||||
const { options, mode } = buildLogOnOptions(config, fsModule, refreshTokenPath);
|
||||
|
||||
logger.info('steam logon started', {
|
||||
reason,
|
||||
mode,
|
||||
steamID: config.steamID,
|
||||
});
|
||||
|
||||
steamUser.logOn(options);
|
||||
}
|
||||
|
||||
function scheduleRetry(err) {
|
||||
if (retryTimer) {
|
||||
logger.warn('steam reconnect already scheduled', {
|
||||
delayMs: retryDelayMs,
|
||||
error: err && err.message,
|
||||
eresult: err && err.eresult,
|
||||
eresultName: err && getEResultName(err.eresult),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const delayMs = retryDelayMs;
|
||||
logger.warn('steam reconnect scheduled', {
|
||||
delayMs,
|
||||
error: err && err.message,
|
||||
eresult: err && err.eresult,
|
||||
eresultName: err && getEResultName(err.eresult),
|
||||
});
|
||||
|
||||
retryTimer = setTimeoutFn(() => {
|
||||
retryTimer = null;
|
||||
retryDelayMs = Math.min(retryDelayMs * 2, maxRetryDelayMs);
|
||||
|
||||
try {
|
||||
logOn('retry');
|
||||
} catch (retryErr) {
|
||||
logger.error('steam reconnect attempt failed to start', {
|
||||
error: retryErr.message,
|
||||
});
|
||||
scheduleRetry(retryErr);
|
||||
}
|
||||
}, delayMs);
|
||||
}
|
||||
|
||||
steamUser.setOption('renewRefreshTokens', true);
|
||||
|
||||
steamUser.on('refreshToken', (refreshToken) => {
|
||||
logger.info('steam refresh token received');
|
||||
|
||||
try {
|
||||
fsModule.writeFileSync(refreshTokenPath, refreshToken);
|
||||
} catch (err) {
|
||||
logger.error('failed to write steam refresh token', {
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
steamUser.on('loggedOn', () => {
|
||||
clearRetryTimer();
|
||||
retryDelayMs = initialRetryDelayMs;
|
||||
loginResolved = true;
|
||||
|
||||
logger.info(`login to Steam as ${steamUser.steamID}`);
|
||||
|
||||
try {
|
||||
steamUser.webLogOn();
|
||||
} catch (err) {
|
||||
logger.warn(`failed to start web login: ${err.message}`);
|
||||
}
|
||||
|
||||
resolveLogin();
|
||||
});
|
||||
|
||||
steamUser.on('disconnected', (eresult, msg) => {
|
||||
logger.warn('steam disconnected', {
|
||||
eresult,
|
||||
eresultName: getEResultName(eresult),
|
||||
message: msg,
|
||||
autoRelogin: steamUser.options && steamUser.options.autoRelogin,
|
||||
});
|
||||
});
|
||||
|
||||
steamUser.on('error', (err) => {
|
||||
const recoverable = isRecoverableSteamError(err);
|
||||
|
||||
logger.error('steam error', {
|
||||
error: err && err.message,
|
||||
eresult: err && err.eresult,
|
||||
eresultName: err && getEResultName(err.eresult),
|
||||
recoverable,
|
||||
});
|
||||
|
||||
if (recoverable) {
|
||||
scheduleRetry(err);
|
||||
return;
|
||||
}
|
||||
|
||||
clearRetryTimer();
|
||||
|
||||
if (!loginResolved && !loginRejected) {
|
||||
loginRejected = true;
|
||||
rejectLogin(err);
|
||||
}
|
||||
});
|
||||
|
||||
steamUser.on('webSession', (sessionID, cookies) => {
|
||||
logger.info(`web session received: ${sessionID}`);
|
||||
|
||||
steamCommunity.setCookies(cookies);
|
||||
if (config.identitySecret && !confirmationCheckerStarted) {
|
||||
try {
|
||||
steamCommunity.startConfirmationChecker(10000, config.identitySecret);
|
||||
confirmationCheckerStarted = true;
|
||||
} catch (err) {
|
||||
logger.warn('failed to start confirmation checker', {
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
resolveWebLogin();
|
||||
});
|
||||
|
||||
logOn('initial');
|
||||
|
||||
return {
|
||||
steamLoginPromise,
|
||||
steamWebLoginPromise,
|
||||
isRecoverableSteamError,
|
||||
buildLogOnOptions,
|
||||
getEResultName,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_INITIAL_RETRY_DELAY_MS,
|
||||
DEFAULT_MAX_RETRY_DELAY_MS,
|
||||
buildLogOnOptions,
|
||||
createSteamLifecycle,
|
||||
getEResultName,
|
||||
isRecoverableSteamError,
|
||||
};
|
||||
321
test/steam-lifecycle.test.js
Normal file
321
test/steam-lifecycle.test.js
Normal file
@@ -0,0 +1,321 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { EventEmitter } = require('node:events');
|
||||
|
||||
const EResult = require('steam-user/enums/EResult');
|
||||
const {
|
||||
buildLogOnOptions,
|
||||
createSteamLifecycle,
|
||||
isRecoverableSteamError,
|
||||
} = require('../steam-lifecycle');
|
||||
|
||||
class FakeSteamUser extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
this.options = { autoRelogin: true };
|
||||
this.logOnCalls = [];
|
||||
this.webLogOnCalls = 0;
|
||||
this.steamID = null;
|
||||
}
|
||||
|
||||
setOption(name, value) {
|
||||
this.options[name] = value;
|
||||
}
|
||||
|
||||
logOn(options) {
|
||||
this.logOnCalls.push(options);
|
||||
}
|
||||
|
||||
webLogOn() {
|
||||
this.webLogOnCalls += 1;
|
||||
}
|
||||
}
|
||||
|
||||
function createLogger() {
|
||||
const entries = [];
|
||||
return {
|
||||
entries,
|
||||
info(message, meta) {
|
||||
entries.push({ level: 'info', message, meta });
|
||||
},
|
||||
warn(message, meta) {
|
||||
entries.push({ level: 'warn', message, meta });
|
||||
},
|
||||
error(message, meta) {
|
||||
entries.push({ level: 'error', message, meta });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createFsModule(token = '') {
|
||||
const writes = [];
|
||||
return {
|
||||
writes,
|
||||
readFileSync(path, encoding) {
|
||||
assert.equal(path, 'refresh.token');
|
||||
assert.equal(encoding, 'utf8');
|
||||
if (!token) {
|
||||
const err = new Error('missing');
|
||||
err.code = 'ENOENT';
|
||||
throw err;
|
||||
}
|
||||
return token;
|
||||
},
|
||||
writeFileSync(path, data) {
|
||||
writes.push({ path, data });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createTimerControls() {
|
||||
const timers = [];
|
||||
return {
|
||||
timers,
|
||||
setTimeoutFn(fn, delayMs) {
|
||||
const timer = { fn, delayMs, cleared: false };
|
||||
timers.push(timer);
|
||||
return timer;
|
||||
},
|
||||
clearTimeoutFn(timer) {
|
||||
timer.cleared = true;
|
||||
},
|
||||
runNextTimer() {
|
||||
const timer = timers.find((item) => !item.cleared && !item.ran);
|
||||
assert.ok(timer, 'expected a pending timer');
|
||||
timer.ran = true;
|
||||
timer.fn();
|
||||
return timer;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createLifecycle(overrides = {}) {
|
||||
const steamUser = overrides.steamUser || new FakeSteamUser();
|
||||
const steamCommunity = overrides.steamCommunity || {
|
||||
cookies: [],
|
||||
checkerCalls: [],
|
||||
setCookies(cookies) {
|
||||
this.cookies.push(cookies);
|
||||
},
|
||||
startConfirmationChecker(intervalMs, identitySecret) {
|
||||
this.checkerCalls.push({ intervalMs, identitySecret });
|
||||
},
|
||||
};
|
||||
const logger = overrides.logger || createLogger();
|
||||
const fsModule = overrides.fsModule || createFsModule(overrides.refreshToken);
|
||||
const timerControls = overrides.timerControls || createTimerControls();
|
||||
const config = {
|
||||
accountName: 'account',
|
||||
password: 'password',
|
||||
logonID: 123,
|
||||
steamID: 'self-id',
|
||||
identitySecret: 'identity-secret',
|
||||
...overrides.config,
|
||||
};
|
||||
|
||||
const lifecycle = createSteamLifecycle({
|
||||
steamUser,
|
||||
steamCommunity,
|
||||
logger,
|
||||
config,
|
||||
fsModule,
|
||||
setTimeoutFn: timerControls.setTimeoutFn,
|
||||
clearTimeoutFn: timerControls.clearTimeoutFn,
|
||||
initialRetryDelayMs: 10,
|
||||
maxRetryDelayMs: 40,
|
||||
});
|
||||
|
||||
return {
|
||||
lifecycle,
|
||||
steamUser,
|
||||
steamCommunity,
|
||||
logger,
|
||||
fsModule,
|
||||
timerControls,
|
||||
};
|
||||
}
|
||||
|
||||
test('buildLogOnOptions uses refresh token when available', () => {
|
||||
const fsModule = createFsModule(' token-value \n');
|
||||
|
||||
assert.deepEqual(buildLogOnOptions({
|
||||
accountName: 'account',
|
||||
password: 'password',
|
||||
logonID: 7,
|
||||
steamID: 'self-id',
|
||||
}, fsModule), {
|
||||
mode: 'refreshToken',
|
||||
options: {
|
||||
logonID: 7,
|
||||
refreshToken: 'token-value',
|
||||
steamID: 'self-id',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('buildLogOnOptions falls back to credentials without a refresh token', () => {
|
||||
const fsModule = createFsModule('');
|
||||
|
||||
assert.deepEqual(buildLogOnOptions({
|
||||
accountName: 'account',
|
||||
password: 'password',
|
||||
logonID: 7,
|
||||
steamID: 'self-id',
|
||||
}, fsModule), {
|
||||
mode: 'credentials',
|
||||
options: {
|
||||
accountName: 'account',
|
||||
password: 'password',
|
||||
logonID: 7,
|
||||
steamID: 'self-id',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('steam disconnected logs warning and lets steam-user autoRelogin handle reconnect', () => {
|
||||
const { steamUser, logger } = createLifecycle();
|
||||
|
||||
assert.equal(steamUser.logOnCalls.length, 1);
|
||||
|
||||
steamUser.emit('disconnected', EResult.NoConnection, 'connection closed');
|
||||
|
||||
assert.equal(steamUser.logOnCalls.length, 1);
|
||||
assert.deepEqual(logger.entries.find((entry) => entry.message === 'steam disconnected'), {
|
||||
level: 'warn',
|
||||
message: 'steam disconnected',
|
||||
meta: {
|
||||
eresult: EResult.NoConnection,
|
||||
eresultName: 'NoConnection',
|
||||
message: 'connection closed',
|
||||
autoRelogin: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('recoverable steam errors schedule a single retry and re-log on', () => {
|
||||
const { steamUser, logger, timerControls } = createLifecycle({
|
||||
refreshToken: 'refresh-token',
|
||||
});
|
||||
|
||||
const err = new Error('No Steam servers available');
|
||||
err.eresult = EResult.NoConnection;
|
||||
|
||||
steamUser.emit('error', err);
|
||||
steamUser.emit('error', err);
|
||||
|
||||
assert.equal(timerControls.timers.length, 1);
|
||||
assert.equal(timerControls.timers[0].delayMs, 10);
|
||||
assert.equal(steamUser.logOnCalls.length, 1);
|
||||
assert.equal(logger.entries.filter((entry) => entry.message === 'steam reconnect scheduled').length, 1);
|
||||
assert.equal(logger.entries.filter((entry) => entry.message === 'steam reconnect already scheduled').length, 1);
|
||||
|
||||
timerControls.runNextTimer();
|
||||
|
||||
assert.equal(steamUser.logOnCalls.length, 2);
|
||||
assert.deepEqual(steamUser.logOnCalls[1], {
|
||||
logonID: 123,
|
||||
refreshToken: 'refresh-token',
|
||||
steamID: 'self-id',
|
||||
});
|
||||
});
|
||||
|
||||
test('non-recoverable initial steam errors reject login and do not retry', async () => {
|
||||
const { lifecycle, steamUser, timerControls } = createLifecycle();
|
||||
const err = new Error('InvalidPassword');
|
||||
err.eresult = EResult.InvalidPassword;
|
||||
|
||||
steamUser.emit('error', err);
|
||||
|
||||
await assert.rejects(lifecycle.steamLoginPromise, err);
|
||||
assert.equal(timerControls.timers.length, 0);
|
||||
});
|
||||
|
||||
test('loggedOn clears pending retry, resets backoff, resolves login, and starts web login', async () => {
|
||||
const { lifecycle, steamUser, timerControls } = createLifecycle({
|
||||
refreshToken: 'refresh-token',
|
||||
});
|
||||
const err = new Error('timeout');
|
||||
err.eresult = EResult.Timeout;
|
||||
|
||||
steamUser.emit('error', err);
|
||||
assert.equal(timerControls.timers.length, 1);
|
||||
|
||||
steamUser.steamID = 'self-id';
|
||||
steamUser.emit('loggedOn');
|
||||
await lifecycle.steamLoginPromise;
|
||||
|
||||
assert.equal(timerControls.timers[0].cleared, true);
|
||||
assert.equal(steamUser.webLogOnCalls, 1);
|
||||
|
||||
steamUser.emit('error', err);
|
||||
assert.equal(timerControls.timers.length, 2);
|
||||
assert.equal(timerControls.timers[1].delayMs, 10);
|
||||
});
|
||||
|
||||
test('webSession refreshes cookies repeatedly but starts confirmation checker once', async () => {
|
||||
const { lifecycle, steamUser, steamCommunity } = createLifecycle();
|
||||
|
||||
steamUser.emit('webSession', 'session-1', ['cookie-1']);
|
||||
steamUser.emit('webSession', 'session-2', ['cookie-2']);
|
||||
|
||||
await lifecycle.steamWebLoginPromise;
|
||||
assert.deepEqual(steamCommunity.cookies, [['cookie-1'], ['cookie-2']]);
|
||||
assert.deepEqual(steamCommunity.checkerCalls, [{
|
||||
intervalMs: 10000,
|
||||
identitySecret: 'identity-secret',
|
||||
}]);
|
||||
});
|
||||
|
||||
test('refreshToken writes token and logs write failures without creating connection log files', () => {
|
||||
const fsModule = createFsModule();
|
||||
const logger = createLogger();
|
||||
const { steamUser } = createLifecycle({ fsModule, logger });
|
||||
|
||||
steamUser.emit('refreshToken', 'new-token');
|
||||
|
||||
assert.deepEqual(fsModule.writes, [{
|
||||
path: 'refresh.token',
|
||||
data: 'new-token',
|
||||
}]);
|
||||
|
||||
const failingFsModule = {
|
||||
readFileSync() {
|
||||
const err = new Error('missing');
|
||||
err.code = 'ENOENT';
|
||||
throw err;
|
||||
},
|
||||
writeFileSync() {
|
||||
throw new Error('disk full');
|
||||
},
|
||||
};
|
||||
const failingLogger = createLogger();
|
||||
const { steamUser: failingSteamUser } = createLifecycle({
|
||||
fsModule: failingFsModule,
|
||||
logger: failingLogger,
|
||||
});
|
||||
|
||||
failingSteamUser.emit('refreshToken', 'new-token');
|
||||
|
||||
assert.equal(
|
||||
failingLogger.entries.some((entry) => entry.message === 'failed to write steam refresh token'),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test('isRecoverableSteamError classifies expected recoverable and fatal errors', () => {
|
||||
const networkErr = new Error('socket disconnected before secure TLS connection was established');
|
||||
networkErr.eresult = EResult.Fail;
|
||||
assert.equal(isRecoverableSteamError(networkErr), true);
|
||||
|
||||
const authErr = new Error('InvalidPassword');
|
||||
authErr.eresult = EResult.InvalidPassword;
|
||||
assert.equal(isRecoverableSteamError(authErr), false);
|
||||
|
||||
const replacedSessionErr = new Error('LoggedInElsewhere');
|
||||
replacedSessionErr.eresult = EResult.LoggedInElsewhere;
|
||||
assert.equal(isRecoverableSteamError(replacedSessionErr), false);
|
||||
|
||||
const throttledErr = new Error('AccountLoginDeniedThrottle');
|
||||
throttledErr.eresult = EResult.AccountLoginDeniedThrottle;
|
||||
assert.equal(isRecoverableSteamError(throttledErr), false);
|
||||
});
|
||||
Reference in New Issue
Block a user