Add failure scenarios tests for connection, appendToStream, deleteStream (wip)

This commit is contained in:
Nicolas Dextraze
2016-03-11 15:55:27 -08:00
parent b0a0af536d
commit 0b63df85e7
16 changed files with 420 additions and 32 deletions

View File

@ -43,8 +43,12 @@ module.exports.UserCredentials = require('./systemData/userCredentials');
module.exports.EventData = EventData;
module.exports.PersistentSubscriptionSettings = require('./persistentSubscriptionSettings');
module.exports.SystemConsumerStrategies = require('./systemConsumerStrategies');
module.exports.WrongExpectedVersionError = require('./errors/wrongExpectedVersionError');
module.exports.StreamDeletedError = require('./errors/streamDeletedError');
module.exports.AccessDeniedError = require('./errors/accessDeniedError');
module.exports.expectedVersion = expectedVersion;
module.exports.positions = positions;
module.exports.systemMetadata = require('./common/systemMetadata');
// Helper functions
module.exports.createConnection = module.exports.EventStoreConnection.create;

View File

@ -7,6 +7,9 @@ var InspectionResult = require('./../systemData/inspectionResult');
var ClientMessage = require('../messages/clientMessage');
var WriteResult = require('../results').WriteResult;
var Position = require('../results').Position;
var WrongExpectedVersionError = require('../errors/wrongExpectedVersionError');
var StreamDeletedError = require('../errors/streamDeletedError');
var AccessDeniedError = require('../errors/accessDeniedError');
var OperationBase = require('../clientOperations/operationBase');
@ -52,17 +55,16 @@ AppendToStreamOperation.prototype._inspectResponse = function(response) {
this._wasCommitTimeout = true;
return new InspectionResult(InspectionDecision.Retry, "CommitTimeout");
case ClientMessage.OperationResult.WrongExpectedVersion:
var err = ["Append failed due to WrongExpectedVersion. Stream: ", this._stream,", Expected version: ", this._expectedVersion].join('');
this.fail(new Error(err));
this.fail(new WrongExpectedVersionError("Append", this._stream, this._expectedVersion));
return new InspectionResult(InspectionDecision.EndOperation, "WrongExpectedVersion");
case ClientMessage.OperationResult.StreamDeleted:
this.fail(new Error("Stream deleted. Stream: " + this._stream));
this.fail(new StreamDeletedError(this._stream));
return new InspectionResult(InspectionDecision.EndOperation, "StreamDeleted");
case ClientMessage.OperationResult.InvalidTransaction:
this.fail(new Error("Invalid transaction."));
return new InspectionResult(InspectionDecision.EndOperation, "InvalidTransaction");
case ClientMessage.OperationResult.AccessDenied:
this.fail(new Error(["Write access denied for stream '", this._stream, "'."].join('')));
this.fail(new AccessDeniedError("Write", this._stream));
return new InspectionResult(InspectionDecision.EndOperation, "AccessDenied");
default:
throw new Error("Unexpected OperationResult: " + response.result);

View File

@ -5,7 +5,7 @@ var TcpCommand = require('../systemData/tcpCommand');
var TcpFlags = require('../systemData/tcpFlags');
var InspectionDecision = require('../systemData/inspectionDecision');
var ClientMessage = require('../messages/clientMessage');
var createInspectionResult = require('./../systemData/inspectionResult');
var InspectionResult = require('./../systemData/inspectionResult');
var createBufferSegment = require('../common/bufferSegment');
function OperationBase(log, cb, requestCommand, responseCommand, userCredentials) {
@ -79,7 +79,7 @@ OperationBase.prototype.inspectPackage = function(pkg) {
}
} catch(e) {
this.fail(e);
return createInspectionResult(InspectionDecision.EndOperation, "Error - " + e.message);
return new InspectionResult(InspectionDecision.EndOperation, "Error - " + e.message);
}
};
@ -91,7 +91,7 @@ OperationBase.prototype._inspectNotAuthenticated = function(pkg)
} catch(e) {}
//TODO typed error
this.fail(new Error("Authentication error: " + message));
return createInspectionResult(InspectionDecision.EndOperation, "NotAuthenticated");
return new InspectionResult(InspectionDecision.EndOperation, "NotAuthenticated");
};
OperationBase.prototype._inspectBadRequest = function(pkg)
@ -102,7 +102,7 @@ OperationBase.prototype._inspectBadRequest = function(pkg)
} catch(e) {}
//TODO typed error
this.fail(new Error("Bad request: " + message));
return createInspectionResult(InspectionDecision.EndOperation, "BadRequest - " + message);
return new InspectionResult(InspectionDecision.EndOperation, "BadRequest - " + message);
};
OperationBase.prototype._inspectNotHandled = function(pkg)
@ -111,20 +111,20 @@ OperationBase.prototype._inspectNotHandled = function(pkg)
switch (message.reason)
{
case ClientMessage.NotHandled.NotHandledReason.NotReady:
return createInspectionResult(InspectionDecision.Retry, "NotHandled - NotReady");
return new InspectionResult(InspectionDecision.Retry, "NotHandled - NotReady");
case ClientMessage.NotHandled.NotHandledReason.TooBusy:
return createInspectionResult(InspectionDecision.Retry, "NotHandled - TooBusy");
return new InspectionResult(InspectionDecision.Retry, "NotHandled - TooBusy");
case ClientMessage.NotHandled.NotHandledReason.NotMaster:
var masterInfo = ClientMessage.NotHandled.MasterInfo.decode(message.additional_info);
return new InspectionResult(InspectionDecision.Reconnect, "NotHandled - NotMaster",
return new new InspectionResult(InspectionDecision.Reconnect, "NotHandled - NotMaster",
{host: masterInfo.external_tcp_address, port: masterInfo.external_tcp_port},
{host: masterInfo.external_secure_tcp_address, port: masterInfo.external_secure_tcp_port});
default:
this.log.error("Unknown NotHandledReason: %s.", message.reason);
return createInspectionResult(InspectionDecision.Retry, "NotHandled - <unknown>");
return new InspectionResult(InspectionDecision.Retry, "NotHandled - <unknown>");
}
};
@ -141,7 +141,7 @@ OperationBase.prototype._inspectUnexpectedCommand = function(pkg, expectedComman
this.constructor.name, this, pkg.data);
this.fail(new Error(util.format("Unexpected command. Expecting %s got %s.", TcpCommand.getName(expectedCommand), TcpCommand.getName(pkg.command))));
return createInspectionResult(InspectionDecision.EndOperation, "Unexpected command - " + TcpCommand.getName(pkg.command));
return new InspectionResult(InspectionDecision.EndOperation, "Unexpected command - " + TcpCommand.getName(pkg.command));
};

View File

@ -0,0 +1,39 @@
var util = require('util');
var fs = require('fs');
var os = require('os');
function FileLogger(filePath, append) {
this._filePath = filePath;
if (!append) {
try {
fs.unlink(filePath);
} catch(e) {}
}
}
function createLine(level, args, argsStartIndex) {
var msg = util.format.apply(util, Array.prototype.slice.call(args, argsStartIndex));
return util.format('%s %s - %s%s', new Date().toISOString(), level, msg, os.EOL);
}
FileLogger.prototype.debug = function() {
var line = createLine('DEBUG', arguments, 0);
fs.appendFileSync(this._filePath, line);
};
FileLogger.prototype.info = function() {
var line = createLine('INFO', arguments, 0);
fs.appendFileSync(this._filePath, line);
};
FileLogger.prototype.error = function(e) {
var hasError = e instanceof Error;
var line = createLine('ERROR', arguments, hasError ? 1 : 0);
if (hasError) {
line += e.stack + os.EOL;
}
fs.appendFileSync(this._filePath, line);
};
module.exports = FileLogger;

View File

@ -0,0 +1,16 @@
const SystemMetadata = {
maxAge: '$maxAge',
maxCount: '$maxCount',
truncateBefore: '$tb',
cacheControl: '$cacheControl',
acl: '$acl',
aclRead: '$r',
aclWrite: '$w',
aclDelete: '$d',
aclMetaRead: '$mr',
aclMetaWrite: '$mw',
userStreamAcl: '$userStreamAcl',
systemStreamAcl: '$systemStreamAcl'
};
module.exports = SystemMetadata;

View File

@ -8,4 +8,16 @@ module.exports.notNullOrEmpty = function(value, name) {
module.exports.notNull = function(value, name) {
if (value === null)
throw new Error(name + " is null.");
};
module.exports.isInteger = function(value, name) {
if (typeof value !== 'number' || value % 1 !== 0)
throw new TypeError(name + " is not an integer.");
};
module.exports.isArrayOf = function(expectedType, value, name) {
if (!Array.isArray(value))
throw new TypeError(name + " is not an array.");
if (!value.every(function(x) { return x instanceof expectedType; }))
throw new TypeError([name, " is not an array of ", expectedType, "."].join(""));
};

View File

@ -115,15 +115,15 @@ EventStoreConnectionLogicHandler.prototype.enqueueMessage = function(msg) {
EventStoreConnectionLogicHandler.prototype._discoverEndpoint = function(cb) {
this._logDebug('DiscoverEndpoint');
if (this._state != ConnectionState.Connecting) return;
if (this._connectingPhase != ConnectingPhase.Reconnecting) return;
if (this._state !== ConnectionState.Connecting) return;
if (this._connectingPhase !== ConnectingPhase.Reconnecting) return;
this._connectingPhase = ConnectingPhase.EndPointDiscovery;
cb = cb || function() {};
var self = this;
this._endpointDiscoverer.discover(this._connection != null ? this._connection.remoteEndPoint : null)
this._endpointDiscoverer.discover(this._connection !== null ? this._connection.remoteEndPoint : null)
.then(function(nodeEndpoints){
self.enqueueMessage(new messages.EstablishTcpConnectionMessage(nodeEndpoints));
cb();
@ -581,7 +581,6 @@ EventStoreConnectionLogicHandler.prototype._timerTick = function() {
{
case ConnectionState.Init: break;
case ConnectionState.Connecting:
{
if (this._connectingPhase == ConnectingPhase.Reconnecting && Date.now() - this._reconnInfo.timeStamp >= this._settings.reconnectionDelay)
{
this._logDebug("TimerTick checking reconnection...");
@ -595,17 +594,15 @@ EventStoreConnectionLogicHandler.prototype._timerTick = function() {
this._discoverEndpoint(null);
}
}
if (this._connectingPhase == ConnectingPhase.Authentication && Date.now() - this._authInfo.timeStamp >= this._settings.operationTimeout)
else if (this._connectingPhase == ConnectingPhase.Authentication && Date.now() - this._authInfo.timeStamp >= this._settings.operationTimeout)
{
this.emit('authenticationFailed', "Authentication timed out.");
this._goToConnectedState();
}
if (this._connectingPhase > ConnectingPhase.ConnectionEstablishing)
else if (this._connectingPhase > ConnectingPhase.ConnectionEstablishing)
this._manageHeartbeats();
break;
}
case ConnectionState.Connected:
{
// operations timeouts are checked only if connection is established and check period time passed
if (Date.now() - this._lastTimeoutsTimeStamp >= this._settings.operationTimeoutCheckPeriod)
{
@ -619,7 +616,6 @@ EventStoreConnectionLogicHandler.prototype._timerTick = function() {
}
this._manageHeartbeats();
break;
}
case ConnectionState.Closed:
break;
default:

View File

@ -0,0 +1,12 @@
var util = require('util');
function AccessDeniedError(action, stream) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.message = util.format("%s access denied for stream '%s'.", action, stream);
this.action = action;
this.stream = stream;
}
util.inherits(AccessDeniedError, Error);
module.exports = AccessDeniedError;

View File

@ -0,0 +1,11 @@
var util = require('util');
function StreamDeletedError(stream) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.message = util.format("Event stream '%s' is deleted.", stream);
this.stream = stream;
}
util.inherits(StreamDeletedError, Error);
module.exports = StreamDeletedError;

View File

@ -0,0 +1,13 @@
var util = require('util');
function WrongExpectedVersionError(action, stream, expectedVersion) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.message = util.format("%s failed due to WrongExpectedVersion. Stream: %s Expected version: %d.", action, stream, expectedVersion);
this.action = action;
this.stream = stream;
this.expectedVersion = expectedVersion;
}
util.inherits(WrongExpectedVersionError, Error);
module.exports = WrongExpectedVersionError;

View File

@ -96,8 +96,10 @@ EventStoreNodeConnection.prototype.close = function() {
* @returns {Promise.<DeleteResult>}
*/
EventStoreNodeConnection.prototype.deleteStream = function(stream, expectedVersion, hardDelete, userCredentials) {
if (typeof stream !== 'string' || stream === '') throw new TypeError("stream must be an non-empty string.");
if (typeof expectedVersion !== 'number' || expectedVersion % 1 !== 0) throw new TypeError("expectedVersion must be an integer.");
ensure.notNullOrEmpty(stream, "stream");
ensure.isInteger(expectedVersion, "expectedVersion");
hardDelete = !!hardDelete;
userCredentials = userCredentials || null;
var self = this;
return new Promise(function(resolve, reject) {
@ -107,8 +109,7 @@ EventStoreNodeConnection.prototype.deleteStream = function(stream, expectedVersi
}
var deleteStreamOperation = new DeleteStreamOperation(
self._settings.log, cb, self._settings.requireMaster, stream, expectedVersion,
!!hardDelete, userCredentials || null);
self._settings.log, cb, self._settings.requireMaster, stream, expectedVersion, hardDelete, userCredentials);
self._enqueueOperation(deleteStreamOperation);
});
};
@ -117,14 +118,17 @@ EventStoreNodeConnection.prototype.deleteStream = function(stream, expectedVersi
* Append events to a stream (async)
* @param {string} stream The name of the stream to which to append.
* @param {number} expectedVersion The version at which we currently expect the stream to be in order that an optimistic concurrency check can be performed.
* @param {Array.<EventData>} events The events to append.
* @param {EventData[]|EventData} events The event(s) to append.
* @param {UserCredentials} [userCredentials] User credentials
* @returns {Promise.<WriteResult>}
*/
EventStoreNodeConnection.prototype.appendToStream = function(stream, expectedVersion, events, userCredentials) {
if (typeof stream !== 'string' || stream === '') throw new TypeError("stream must be an non-empty string.");
if (typeof expectedVersion !== 'number' || expectedVersion % 1 !== 0) throw new TypeError("expectedVersion must be an integer.");
if (!Array.isArray(events)) throw new TypeError("events must be an array.");
ensure.notNullOrEmpty(stream, "stream");
ensure.isInteger(expectedVersion, "expectedVersion");
if (!Array.isArray(events))
events = [events];
ensure.isArrayOf(EventData, events, "events");
userCredentials = userCredentials || null;
var self = this;
return new Promise(function(resolve, reject) {
@ -133,7 +137,7 @@ EventStoreNodeConnection.prototype.appendToStream = function(stream, expectedVer
resolve(result);
}
var operation = new AppendToStreamOperation(self._settings.log, cb, self._settings.requireMaster, stream,
expectedVersion, events, userCredentials || null);
expectedVersion, events, userCredentials);
self._enqueueOperation(operation);
});
};
@ -590,7 +594,7 @@ EventStoreNodeConnection.prototype.getStreamMetadataRaw = function(stream, userC
case results.EventReadStatus.NoStream:
return new results.RawStreamMetadataResult(stream, false, -1, null);
case results.EventReadStatus.StreamDeleted:
return new results.RawStreamMetadataResult(stream, true, Number.MAX_VALUE, null);
return new results.RawStreamMetadataResult(stream, true, 0x7fffffff, null);
default:
throw new Error(util.format("Unexpected ReadEventResult: %s.", res.status));
}

View File

@ -16,11 +16,13 @@ function TcpConnection(log, connectionId, remoteEndPoint, onConnectionClosed) {
this._receiveQueue = [];
Object.defineProperty(this, 'remoteEndPoint', {
enumerable: true,
get: function() {
return this._remoteEndPoint;
}
});
Object.defineProperty(this, 'localEndPoint', {
enumerable: true,
get: function() {
return this._localEndPoint;
}

View File

@ -67,21 +67,25 @@ function TcpPackageConnection(
});
}
Object.defineProperty(TcpPackageConnection.prototype, 'connectionId', {
enumerable: true,
get: function() {
return this._connectionId;
}
});
Object.defineProperty(TcpPackageConnection.prototype, 'isClosed', {
enumerable: true,
get: function() {
return this._connection.isClosed;
}
});
Object.defineProperty(TcpPackageConnection.prototype, 'remoteEndPoint', {
enumerable: true,
get: function() {
return this._connection.remoteEndPoint;
}
});
Object.defineProperty(TcpPackageConnection.prototype, 'localEndPoint', {
enumerable: true,
get: function() {
return this._connection.localEndPoint;
}