Skip to content

Commit

Permalink
Additional tests; remove unreachable branches.
Browse files Browse the repository at this point in the history
  • Loading branch information
autopulated committed May 1, 2024
1 parent 5f206c0 commit 7e9f7ef
Show file tree
Hide file tree
Showing 5 changed files with 259 additions and 93 deletions.
28 changes: 13 additions & 15 deletions lib/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ class BaseModel {
const validate = schema[kSchemaCompiled];
const valid = validate(params);
if (!valid) {
const e = new Error(`Document does not match schema for ${schema.name}: ${validate.errors[0]?.instancePath ?? ''} ${validate.errors[0]?.message}.`);
const e = new Error(`Document does not match schema for ${schema.name}: ${validate.errors[0].instancePath} ${validate.errors[0].message}.`);
e.validationErrors = validate.errors;
throw e;
}
Expand Down Expand Up @@ -321,7 +321,7 @@ class BaseModel {
if(!BaseModel.#queryMany_options_validate(options)){
throw new Error(`Invalid options: ${inspect(BaseModel.#queryMany_options_validate.errors, {breakLength:Infinity})}.`);
}
let {rawQueryOptions, rawFetchOptions, ...otherOptions} = options ?? {};
let {rawQueryOptions, rawFetchOptions, ...otherOptions} = options;

// returns an array of models (possibly empty)
const rawQuery = BaseModel.#convertQuery(this, query, Object.assign({startAfter: otherOptions.startAfter, limit: otherOptions.limit}, rawQueryOptions));
Expand Down Expand Up @@ -355,7 +355,7 @@ class BaseModel {
throw new Error(`Invalid options: ${inspect(BaseModel.#queryManyIds_options_validate.errors, {breakLength:Infinity})}.`);
}
// options are as queryMany, except Ids are returned, so there are no rawFetchOptions
let {rawQueryOptions, ...otherOptions} = options ?? {};
let {rawQueryOptions, ...otherOptions} = options;
otherOptions = Object.assign({limit: 50}, otherOptions);
const rawQuery = BaseModel.#convertQuery(this, query, Object.assign({startAfter: otherOptions.startAfter, limit: otherOptions.limit}, rawQueryOptions));
const results = [];
Expand Down Expand Up @@ -418,7 +418,7 @@ class BaseModel {
const marshall = schema[kSchemaMarshall];
const marshallValid = marshall(properties);
if (!marshallValid) {
const e = new Error(`Document does not match schema for ${schema.name}: ${marshall.errors[0]?.instancePath ?? ''} ${marshall.errors[0]?.message}.`);
const e = new Error(`Document does not match schema for ${schema.name}: ${marshall.errors[0].instancePath} ${marshall.errors[0].message}.`);
e.validationErrors = schema[kSchemaCompiled].errors;
throw e;
}
Expand Down Expand Up @@ -555,9 +555,9 @@ class BaseModel {
// if we've loaded a model of a different type, and there is nothing
let e;
if (params.type !== DerivedModel[kModelSchema].name) {
e = new Error(`Document does not match schema for ${schema.name}. The loaded document has a different type "${params.type}", and the schema is incompatible: ${unmarshall.errors[0]?.instancePath ?? ''} ${unmarshall.errors[0]?.message}.`);
e = new Error(`Document does not match schema for ${schema.name}. The loaded document has a different type "${params.type}", and the schema is incompatible: ${unmarshall.errors[0].instancePath} ${unmarshall.errors[0].message}.`);
} else {
e = new Error(`Document does not match schema for ${schema.name}: ${unmarshall.errors[0]?.instancePath ?? ''} ${unmarshall.errors[0]?.message}.`);
e = new Error(`Document does not match schema for ${schema.name}: ${unmarshall.errors[0].instancePath} ${unmarshall.errors[0].message}.`);
}
e.validationErrors = unmarshall.errors;
throw e;
Expand All @@ -570,8 +570,7 @@ class BaseModel {

// get an instance of this schema by id
static async #getById(DerivedModel, id, rawOptions) {
// only the ConsistentRead option is supported
const { ConsistentRead, abortSignal } = rawOptions ?? {};
const { ConsistentRead, abortSignal } = rawOptions;
const table = DerivedModel[kModelTable];
const schema = DerivedModel[kModelSchema];
/* c8 ignore next 3 */
Expand Down Expand Up @@ -602,8 +601,7 @@ class BaseModel {

// get an array of instances of this schema by id
static async #getByIds(DerivedModel, ids, rawOptions) {
// only the ConsistentRead option is supported
const { ConsistentRead, abortSignal } = rawOptions ?? {};
const { ConsistentRead, abortSignal } = rawOptions;
const table = DerivedModel[kModelTable];
const schema = DerivedModel[kModelSchema];
/* c8 ignore next 3 */
Expand Down Expand Up @@ -672,7 +670,7 @@ class BaseModel {
// ids of the stored records that match the query. Use getById to get the
// object for each ID.
static async* #rawQueryIds(DerivedModel, rawQuery, options) {
const {limit, abortSignal} = options?? {};
const {limit, abortSignal} = options;
const table = DerivedModel[kModelTable];
const schema = DerivedModel[kModelSchema];
/* c8 ignore next 3 */
Expand Down Expand Up @@ -708,7 +706,7 @@ class BaseModel {
// query for instances of this schema, an async generator returning arrays of ids matching the query, up to options.limit.
// rawQueryIdsBatchIterator does NOT set the TableName, unlike other #rawQuery* APIs.
static async* #rawQueryIdsBatchIterator(DerivedModel, rawQuery, options) {
const {limit, abortSignal} = options?? {};
const {limit, abortSignal} = options;
const table = DerivedModel[kModelTable];
const schema = DerivedModel[kModelSchema];
/* c8 ignore next 3 */
Expand All @@ -719,7 +717,7 @@ class BaseModel {
...(abortSignal && {abortSignal})
};
let response;
let limitRemaining = limit ?? Infinity;
let limitRemaining = limit;
do {
const command = new QueryCommand(rawQuery);
DerivedModel[kModelLogger].trace({command}, 'rawQueryIdsBatch');
Expand Down Expand Up @@ -835,7 +833,7 @@ class BaseModel {
// ExpressionAttributeNames and ExpressionAttributeValues), but in
// practise this is not useful without additional projected attributes
// on indexes, so I'm not sure this should be supported.
const {ExpressionAttributeNames, ExpressionAttributeValues, startAfter, ...otherOptions} = options ?? {};
const {ExpressionAttributeNames, ExpressionAttributeValues, startAfter, ...otherOptions} = options;
const table = DerivedModel[kModelTable];
const schema = DerivedModel[kModelSchema];
const queryEntries = this.#queryEntries(query);
Expand Down Expand Up @@ -882,7 +880,7 @@ class BaseModel {
for (const v of entry.values) {
const valid = defaultIgnoringAjv.validate(keySchema, v);
if (!valid) {
const e = new Error(`Value does not match schema for ${entry.key}: ${defaultIgnoringAjv.errors[0]?.instancePath ?? ''} ${defaultIgnoringAjv.errors[0]?.message}.`);
const e = new Error(`Value does not match schema for ${entry.key}: ${defaultIgnoringAjv.errors[0].instancePath} ${defaultIgnoringAjv.errors[0].message}.`);
e.validationErrors = defaultIgnoringAjv.errors;
throw e;
}
Expand Down
2 changes: 1 addition & 1 deletion lib/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ class Schema {
[kSchemaUnMarshall] = null;

constructor(name, schemaSource, options) {
const { index, generateId, versioning } = options ?? {};
const { index, generateId, versioning } = options;
if (['object', 'undefined'].includes(typeof schemaSource) === false) {
throw new Error('Invalid schema: must be an object or undefined.');
}
Expand Down
181 changes: 181 additions & 0 deletions test/model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
const t = require('tap');

const clientOptions = {
endpoint: 'http://localhost:8000'
};

const DynamoDMConstructor = require('../');
const DynamoDM = DynamoDMConstructor({clientOptions, logger:{level:'error'}});

t.test('model:', async t => {
const table = DynamoDM.Table({ name: 'test-table-models'});
const FooSchema = DynamoDM.Schema('namespace.foo', {
properties: {
id: DynamoDM.DocIdField,
fooVal: {type: 'number'},
blob: DynamoDM.Binary,
padding: DynamoDM.Binary
}
});
const BarSchema = DynamoDM.Schema('ambiguous.bar', {
properties: {
id: DynamoDM.DocIdField,
barVal: {type: 'number'},
barValStr: {type: 'string'},
blob: DynamoDM.Binary
},
required:['barVal']
}, {
index: {
barValRange: {
hashKey: 'type',
sortKey: 'barVal'
},
barValRangeStr: {
hashKey: 'type',
sortKey: 'barValStr'
}
}
});
const AmbiguousSchema = DynamoDM.Schema('ambiguous', {
properties: {
id: DynamoDM.DocIdField,
}
});
const Foo = table.model(FooSchema);
table.model(BarSchema);
table.model(AmbiguousSchema);

const all_foos = [];
for (let i = 0; i < 50; i++ ) {
// padd the Foo items out to 350KBk each, so that we can test bumping up against dynamoDB's 16MB response limit
let foo = new Foo({fooVal:i, blob: Buffer.from(`hello query ${i}`), padding: Buffer.alloc(3.5E5)});
all_foos.push(foo);
await foo.save();
}

t.after(async () => {
await table.deleteTable();
table.destroyConnection();
});


t.test('getById', async t => {
t.rejects(Foo.getById(null), 'should reject null id');
t.rejects(Foo.getById('someid', {foo:1}), 'should reject invalid option');
t.rejects(Foo.getById(''), 'should reject empty id');
t.rejects(Foo.getById(123), 'should reject numeric id');
t.end();
});

t.test('getById options', async t => {
t.test('ConsistentRead: true', async t => {
const foo = await Foo.getById(all_foos[0].id, {ConsistentRead: true});
t.ok(foo, 'should return a foo');
t.equal(foo.constructor, Foo, 'should return the correct type');
t.end();
});
t.test('ConsistentRead: false', async t => {
const foo = await Foo.getById(all_foos[0].id, {ConsistentRead: false});
t.ok(foo, 'should return a foo');
t.equal(foo.constructor, Foo, 'should return the correct type');
t.end();
});

t.test('empty options', async t => {
const foo = await Foo.getById(all_foos[0].id, {});
t.ok(foo, 'should return a foo');
t.equal(foo.constructor, Foo, 'should return the correct type');
t.end();
});

t.end();
});

t.test('table.getById', async t => {
t.rejects(table.getById('blegh.someid'), new Error('Table has no matching model type for id "blegh.someid", so it cannot be loaded.'), 'should reject unknown type');
t.rejects(table.getById('ambiguous.bar.someid'), new Error('Table has multiple ambiguous model types for id "ambiguous.bar.someid", so it cannot be loaded generically.'), 'should reject ambiguous type');
const foo = await table.getById(all_foos[0].id);
t.equal(foo.constructor, Foo, 'should get the correct type');
t.equal(foo.id, all_foos[0].id, 'should get the correct document');
});

t.test('getByIds', async t => {
t.rejects(Foo.getByIds([null]), 'should reject null id');
t.rejects(Foo.getByIds(['someid'], {foo:1}), 'should reject invalid option');
t.match(await Foo.getByIds(['nonexistent']), [null], 'should return null for nonexistent id');
t.match(await Foo.getByIds(['nonexistent', all_foos[0].id]), [null, all_foos[0]], 'should return null along with extant model');
const foos = await Foo.getByIds(all_foos.map(f => f.id));
t.equal(foos.length, all_foos.length, 'should return all models');
t.match(foos.map(f => f?.id), all_foos.map(f => f?.id), 'should return all models in order');
t.rejects(Foo.getByIds(''), new Error('Invalid ids: must be array of strings of nonzero length.'), 'should reject non-array argument');
t.end();
});

t.test('getByIds options', async t => {
t.test('ConsistentRead: true', async t => {
const foos = await Foo.getByIds([all_foos[0].id, all_foos[1].id], {ConsistentRead: true});
t.equal(foos.length, 2, 'should return the right number of foos');
t.equal(foos[0].constructor, Foo, 'should return the correct type');
t.end();
});
t.test('ConsistentRead: false', async t => {
const foos = await Foo.getByIds([all_foos[0].id, all_foos[1].id], {ConsistentRead: false});
t.equal(foos.length, 2, 'should return the right number of foos');
t.equal(foos[0].constructor, Foo, 'should return the correct type');
t.end();
});

t.test('empty options', async t => {
const foos = await Foo.getByIds([all_foos[0].id, all_foos[1].id], {});
t.equal(foos.length, 2, 'should return the right number of foos');
t.equal(foos[0].constructor, Foo, 'should return the correct type');
t.end();
});
t.end();
});

t.test('getByIds exceeding retries', async t => {
const table2 = DynamoDM.Table({ name: 'test-table-models', retry: { maxRetries:0 }});
t.after(async () => { table2.destroyConnection(); });
const Foo2 = table2.model(FooSchema);
t.rejects(Foo2.getByIds(all_foos.map(f => f.id)), {message:'Request failed: maximum retries exceeded.'}, 'getByIds with a large number of large responses should require retries for BatchGetCommand.');
t.end();
});

t.test('aborting getByIds', async t => {
const ac0 = new AbortController();
ac0.abort(new Error('my reason 0 '));
// the AWS SDk doesn't propagate the abort reason (but it would be nice if it did in the future)
t.rejects(Foo.getByIds(all_foos.map(f => f.id), {abortSignal: ac0.signal}), {name:'AbortError', message:'Request aborted'}, 'getByIds should be abortable with an AbortController that is already aborted');

const ac1 = new AbortController();
// the AWS SDk doesn't propagate the abort reason (but it would be nice if it did in the future)
t.rejects(Foo.getByIds(all_foos.map(f => f.id), {abortSignal: ac1.signal}), {name:'AbortError', message:'Request aborted'}, 'getByIds should be abortable with an AbortController signal immediately');
ac1.abort(new Error('my reason'));

const ac2 = new AbortController();
t.rejects(Foo.getByIds(all_foos.map(f => f.id), {abortSignal: ac2.signal}), {name:'AbortError', message:'Request aborted'}, 'getByIds should be abortable with an AbortController signal asynchronously');
setTimeout(() => {
ac2.abort(new Error('my reason 2'));
}, 1);
t.end();
});

t.test('aborting getById', async t => {
const ac0 = new AbortController();
ac0.abort(new Error('my reason 0 '));
t.rejects(Foo.getById(all_foos[0].id, {abortSignal: ac0.signal}), {name:'AbortError', message:'Request aborted'}, 'getById should be abortable with an AbortController that is already aborted');

const ac1 = new AbortController();
t.rejects(Foo.getById(all_foos[0].id, {abortSignal: ac1.signal}), {name:'AbortError', message:'Request aborted'}, 'getById should be abortable with an AbortController signal immediately');
ac1.abort(new Error('my reason'));

const ac2 = new AbortController();
t.rejects(Foo.getById(all_foos[0].id, {abortSignal: ac2.signal}), {name:'AbortError', message:'Request aborted'}, 'getById should be abortable with an AbortController signal asynchronously');
setTimeout(() => {
ac2.abort(new Error('my reason 2'));
}, 1);
t.end();
});
});
Loading

0 comments on commit 7e9f7ef

Please sign in to comment.