/** * Token state management test * * Copyright (C) 2010-2019 R-T Specialty, LLC. * * This file is part of the Liza Data Collection Framework. * * liza is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ import { TokenData, TokenQueryResult, TokenStatus, } from "../../../src/server/token/TokenDao"; import { MongoTokenDao as Sut } from "../../../src/server/token/MongoTokenDao"; import { MongoCollection } from "mongodb"; import { TokenId, TokenNamespace, TokenState, } from "../../../src/server/token/Token"; import { DocumentId } from "../../../src/document/Document"; import { UnknownTokenError } from "../../../src/server/token/UnknownTokenError"; import { hasContext } from "../../../src/error/ContextError"; import { expect, use as chai_use } from 'chai'; chai_use( require( 'chai-as-promised' ) ); describe( 'server.token.TokenDao', () => { describe( '#updateToken', () => { const field = 'foo_field'; const did = 12345; const ns = 'namespace'; const tok_id = 'tok123'; const tok_type = TokenState.DONE; const data = "some data"; const timestamp = 12345; const root = field + '.' + ns; const last_tok_id = 'last-tok'; const last: TokenStatus = { type: TokenState.DEAD, timestamp: 4567, data: "last token", }; const prev: TokenStatus = { type: TokenState.ACTIVE, timestamp: 11111, data: "prev status", }; ( <{ label: string, given: TokenQueryResult, expected: TokenData }[]>[ { label: "updates token and returns previous data", given: { [field]: { [ns]: { last: last_tok_id, lastState: { [ prev.type ]: last_tok_id, }, lastStatus: { type: last.type, timestamp: last.timestamp, data: last.data, }, [tok_id]: { status: { type: prev.type, timestamp: prev.timestamp, data: prev.data, }, }, }, }, }, expected: { id: tok_id, status: { type: tok_type, timestamp: timestamp, data: data, }, prev_state: { [ prev.type ]: last_tok_id, }, prev_status: prev, prev_last: { id: last_tok_id, status: last, prev_status: null, prev_last: null, prev_state: {}, }, }, }, { label: "returns null for prev status if missing data", given: { [field]: { [ns]: { last: last_tok_id, lastStatus: { type: last.type, timestamp: last.timestamp, data: last.data, }, }, }, }, expected: { id: tok_id, status: { type: tok_type, timestamp: timestamp, data: data, }, prev_status: null, prev_state: {}, prev_last: { id: last_tok_id, status: last, prev_status: null, prev_last: null, prev_state: {}, }, }, }, { label: "returns null for missing namespace data", given: { [field]: { [ns]: {}, }, }, expected: { id: tok_id, status: { type: tok_type, timestamp: timestamp, data: data, }, prev_status: null, prev_state: {}, prev_last: null, }, }, { label: "returns null for missing namespace", given: { [field]: {}, }, expected: { id: tok_id, status: { type: tok_type, timestamp: timestamp, data: data, }, prev_status: null, prev_state: {}, prev_last: null, }, }, { label: "returns null for missing root field", given: {}, expected: { id: tok_id, status: { type: tok_type, timestamp: timestamp, data: data, }, prev_status: null, prev_state: {}, prev_last: null, }, }, ] ).forEach( ( { given, expected, label } ) => it( label, () => { const coll: MongoCollection = { findAndModify( selector, _sort, given_data, options, callback ) { const expected_entry: TokenStatus = { type: tok_type, timestamp: timestamp, data: data, }; expect( selector.id ).to.equal( did ); expect( given_data ).to.deep.equal( { $set: { [ `${root}.last` ]: tok_id, [ `${root}.lastState.${tok_type}` ]: tok_id, [ `${root}.lastStatus` ]: expected_entry, [ `${root}.${tok_id}.status` ]: expected_entry, }, $push: { [ `${root}.${tok_id}.statusLog` ]: expected_entry, }, } ); expect( options ).to.deep.equal( { upsert: true, new: false, fields: { [ `${root}.last` ]: 1, [ `${root}.lastState` ]: 1, [ `${root}.lastStatus` ]: 1, [ `${root}.${tok_id}.status` ]: 1, }, } ); callback( null, given ); }, update() {}, findOne() {}, find() {}, createIndex() {}, insert() {}, }; return expect( new Sut( coll, field, () => timestamp ) .updateToken( did, ns, tok_id, tok_type, data ) ).to.eventually.deep.equal( expected ); } ) ); it( 'proxies error to callback', () => { const expected_error = Error( "expected error" ); const coll: MongoCollection = { findAndModify( _selector, _sort, _update, _options, callback ) { callback( expected_error, {} ); }, update() {}, findOne() {}, find() {}, createIndex() {}, insert() {}, }; return expect( new Sut( coll, 'foo', () => 0 ).updateToken( 0, 'ns', 'id', TokenState.DONE, null ) ).to.eventually.be.rejectedWith( expected_error ); } ); } ); describe( '#getToken', () => { const field = 'get_field'; const did = 12345; const ns = 'get_ns'; const expected_status: TokenStatus = { type: TokenState.ACTIVE, timestamp: 0, data: "", }; const last_tok_id = 'last-tok'; const last: TokenStatus = { type: TokenState.DEAD, timestamp: 4567, data: "last token", }; ( <[string, TokenId, TokenQueryResult, TokenData|null, any, any][]>[ [ 'retrieves token by id', 'tok123', { [field]: { [ns]: { last: last_tok_id, lastState: { [ TokenState.ACTIVE ]: last_tok_id, [ TokenState.DONE ]: last_tok_id, }, lastStatus: last, tok123: { status: expected_status, statusLog: [ expected_status ], }, }, }, }, { id: 'tok123', status: expected_status, prev_status: expected_status, prev_state: { [ TokenState.ACTIVE ]: last_tok_id, [ TokenState.DONE ]: last_tok_id, }, prev_last: { id: last_tok_id, status: last, prev_status: null, prev_last: null, prev_state: {}, } }, null, null, ], [ 'rejects for namespace if token is not found', 'tok123', { [field]: { [ns]: { last: last_tok_id, lastStatus: last, // just to make sure we don't grab another tok othertok: { status: expected_status, statusLog: [ expected_status ], }, }, }, }, null, `${ns}.tok123`, { doc_id: did, quote_id: did, ns: ns, token_id: 'tok123', }, ], [ 'rejects if namespace is not found', 'tok123', { [field]: {}, }, null, ns, { doc_id: did, quote_id: did, ns: ns, }, ], [ 'returns last modified token given no token id', '', { [field]: { [ns]: { last: last_tok_id, lastState: { [ TokenState.DEAD ]: last_tok_id, }, lastStatus: last, [ last_tok_id ]: { status: expected_status, statusLog: [ expected_status ], }, }, }, }, { id: last_tok_id, status: last, prev_status: last, prev_state: { [ TokenState.DEAD ]: last_tok_id, }, prev_last: { id: last_tok_id, status: last, prev_status: null, prev_last: null, prev_state: {}, } }, null, null, ], [ 'rejects unknown last modified token given no token id', '', { [field]: { [ns]: {}, }, }, null, ns, { doc_id: did, quote_id: did, ns: ns, }, ], [ 'rejects unknown namespace token given no token id', '', { [field]: {}, }, null, ns, { doc_id: did, quote_id: did, ns: ns, }, ], ] ).forEach( ( [ label, tok_id, dbresult, expected, fmsg, fcontext ] ) => it( label, () => { const coll: MongoCollection = { findOne( selector, { fields }, callback ) { const expected_fields = { [ `${field}.${ns}.last` ]: 1, [ `${field}.${ns}.lastState` ]: 1, [ `${field}.${ns}.lastStatus` ]: 1, }; if ( tok_id ) { expected_fields[ `${field}.${ns}.${tok_id}` ] = 1; } expect( fields ).to.deep.equal( expected_fields ); expect( selector ).to.deep.equal( { id: did } ); callback( null, dbresult ); }, update() {}, findAndModify() {}, find() {}, createIndex() {}, insert() {}, }; const result = new Sut( coll, field, () => 0 ) .getToken( did, ns, tok_id ); return ( fmsg !== null ) ? Promise.all( [ expect( result ).to.eventually.be.rejectedWith( UnknownTokenError, fmsg ), expect( result ).to.eventually.be.rejectedWith( UnknownTokenError, ''+did ), result.catch( e => { if ( !hasContext( e ) ) { // TS will soon have type assertions and // then this conditional and return can be // removed return expect.fail(); } return expect( e.context ).to.deep.equal( fcontext ); } ), ] ) : expect( result ).to.eventually.deep.equal( expected ); } ) ); it( 'proxies error to callback', () => { const expected_error = Error( "expected error" ); const coll: MongoCollection = { findOne( _selector, _fields, callback ) { callback( expected_error, {} ); }, update() {}, findAndModify() {}, find() {}, createIndex() {}, insert() {}, }; return expect( new Sut( coll, 'foo', () => 0 ) .getToken( 0, 'ns', 'id' ) ).to.eventually.be.rejectedWith( expected_error ); } ); } ); } );