import { grpc } from '@improbable-eng/grpc-web' ; import { PrivateKey } from '@textile/crypto' ; import { Where } from '@textile/threads-client' ; import { ThreadID } from '@textile/threads-id' ; import { expect } from 'chai' ; import { Errors , Remote } from '.' ; import { ChangeTableName , StashTableName } from '../middleware/changes' ; import { NewDexie } from '../utils' ; import { shouldHaveThrown } from '../utils/spec.utils' ; import { createDbClient } from './grpc' ; const databaseName = 'remote' ; const serviceHost = 'http://localhost:6007' ; describe ( 'ThreadDB' , function ( ) { describe ( 'remote' , function ( ) { class Dog { constructor ( public _id ? , public name = 'Lucas' , public age = 7 ) { } } const privateKey = PrivateKey . fromRandom ( ) ; const dexie = NewDexie ( databaseName ) ; dexie . version ( 1 ) . stores ( { dogs : '++_id,name,age' , } ) ; dexie . table ( 'dogs' ) . mapToClass ( Dog ) ; const remote = new Remote ( dexie ) ; before ( async function ( ) { await dexie . open ( ) ; } ) ; after ( async function ( ) { dexie . close ( ) ; await dexie . delete ( ) ; } ) ; /**
         * Function to create a set of pre-defined udpates/changes
         */ async function createChanges ( ) { const dogs = dexie . table ( 'dogs' ) ; const lucas = await dogs . put ( new Dog ( ) ) ; await dexie . transaction ( 'rw' , [ 'dogs' ] , async ( tx ) => { const dogs = tx . table ( 'dogs' ) ; const friend = await dogs . get ( lucas ) ; ++ friend . age ; await dogs . put ( friend ) ; await dexie . transaction ( 'rw' , dogs , async ( ) => { const id = await dogs . bulkAdd ( [ new Dog ( undefined , 'Jefferson' , 9 ) , new Dog ( undefined , 'Clark' , 13 ) , ] ) ; const friend = await dogs . get ( id ) ; friend . name = 'Lewis' ; await dogs . put ( friend ) ; await dogs . delete ( id ) ; } ) ; } ) ; } context ( 'init and auth' , function ( ) { beforeEach ( async function ( ) { remote . set ( { serviceHost } ) ; await remote . authorize ( privateKey ) ; } ) ; it ( 'should have authorization token cached in internal metadata' , function ( ) { const [ cached ] = remote . config . metadata ?. get ( 'authorization' ) ?? [ ] ; expect ( cached ) . to . not . be . undefined ; } ) ; it ( 'should throw when authorizing without a remote set' , async function ( ) { const cached = { ... remote . config } ; remote . config . serviceHost = undefined ; try { await remote . authorize ( privateKey ) ; throw shouldHaveThrown ; } catch ( err ) { expect ( err ) . to . equal ( Errors . NoRemoteError ) ; } remote . config = cached ; } ) ; it ( 'should be able to re-authorize without error' , async function ( ) { const token = await remote . authorize ( privateKey ) ; const other = PrivateKey . fromRandom ( ) ; const otherToken = await remote . authorize ( other . public . toString ( ) , ( challenge ) => other . sign ( challenge ) ) ; expect ( otherToken ) . to . not . be . undefined ; expect ( otherToken . length ) . to . equal ( token . length ) ; remote . config . metadata ?. set ( 'authorization' , token ) ; } ) ; it ( 'should be able to get and set config properties' , function ( ) { expect ( remote . config ) . to . not . be . undefined ; expect ( remote . config . serviceHost ) . to . not . be . undefined ; expect ( remote . get ( ) . debug ) . to . equal ( false ) ; remote . set ( { debug : true } ) ; expect ( remote . config . debug ) . to . equal ( true ) ; expect ( remote . get ( ) . debug ) . to . equal ( true ) ; remote . set ( { debug : false } ) ; const metadata = remote . config . metadata ; if ( metadata === undefined ) throw Error ( 'metadata should not be undefined' ) ; const cached = metadata . get ( 'authorization' ) ; expect ( cached ) . to . not . be . undefined ; remote . config . metadata = new grpc . Metadata ( ) ; expect ( remote . config . metadata ?. get ( 'authorization' ) ) . to . deep . equal ( [ ] ) ; remote . config . metadata = metadata ; } ) ; it ( 'should throw if no remote db + thread has been created yet' , async function ( ) { try { await remote . pull ( ) ; throw shouldHaveThrown ; } catch ( err ) { expect ( err ) . to . equal ( Errors . ThreadIDError ) ; } } ) ; it ( 'should create remote db on initialize, and throw when trying to re-create' , async function ( ) { this . timeout ( 5000 ) ; const id0 = ThreadID . fromRandom ( ) . toString ( ) ; const id1 = await remote . initialize ( id0 ) ; expect ( id1 ) . to . not . be . undefined ; expect ( ThreadID . fromString ( id1 ) . toString ( ) ) . to . equal ( id1 ) ; remote . id = undefined ; await remote . initialize ( ) ; const id2 = await remote . initialize ( ThreadID . fromRandom ( ) ) ; expect ( id2 ) . to . not . equal ( id1 ) ; } ) ; } ) ; context ( 'changes and stashing' , function ( ) { beforeEach ( async function ( ) { this . timeout ( 30000 ) ; await createChanges ( ) ; } ) ; it ( 'should stash all changes and clear changes table, and then clear stash' , async function ( ) { const changes = dexie . table ( ChangeTableName ) ; const stash = dexie . table ( StashTableName ) ; expect ( await changes . count ( ) ) . to . equal ( 6 ) ; expect ( await stash . count ( ) ) . to . equal ( 0 ) ; await remote . createStash ( ) ; expect ( await stash . count ( ) ) . to . equal ( 6 ) ; expect ( await changes . count ( ) ) . to . equal ( 0 ) ; await remote . createStash ( ) ; expect ( await stash . count ( ) ) . to . equal ( 6 ) ; await remote . clearStash ( ) ; expect ( await stash . count ( ) ) . to . equal ( 0 ) ; } ) ; it ( 'should be able to create changes, stash them, make more changes, apply stash to overwrite' , async function ( ) { this . timeout ( 30000 ) ; const changes = dexie . table ( ChangeTableName ) ; const stash = dexie . table ( StashTableName ) ; await remote . createStash ( ) ; await dexie . transaction ( 'rw' , [ 'dogs' ] , async ( tx ) => { const dogs = tx . table < Dog > ( 'dogs' ) ; const friend = await dogs . get ( { name : 'Lucas' } ) ; if ( friend === undefined ) throw new Error ( 'should be defined' ) ; friend . name = 'Not Lucas' ; await dogs . put ( friend ) ; } ) ; expect ( await stash . count ( ) ) . to . equal ( 6 ) ; expect ( await changes . count ( ) ) . to . equal ( 1 ) ; const dogs = dexie . table < Dog > ( 'dogs' ) ; expect ( await dogs . get ( { name : 'Not Lucas' } ) ) . to . not . be . undefined ; await remote . applyStash ( 'dogs' ) ; const friend = await dogs . get ( { name : 'Lucas' } ) ; if ( friend === undefined ) throw new Error ( 'should be defined' ) ; const clark = await dogs . get ( { name : 'Clark' } ) ; expect ( clark ) . to . be . undefined ; } ) ; } ) ; context ( 'push and pull' , function ( ) { before ( async function ( ) { remote . set ( { serviceHost } ) ; await remote . authorize ( privateKey ) ; } ) ; beforeEach ( async function ( ) { this . timeout ( 30000 ) ; await remote . initialize ( ThreadID . fromRandom ( ) ) ; await dexie . table ( 'dogs' ) . clear ( ) ; await dexie . table ( ChangeTableName ) . clear ( ) ; await createChanges ( ) ; } ) ; it ( 'should throw when pushing local table that does not exist' , async function ( ) { const changes = dexie . table ( ChangeTableName ) ; expect ( await changes . count ( ) ) . to . equal ( 6 ) ; try { await remote . push ( 'fake' ) ; throw shouldHaveThrown ; } catch ( err ) { expect ( err . toString ( ) ) . to . contain ( 'Table fake does not exist' ) ; } } ) ; it ( 'should push tracked changes to a remote when calling push' , async function ( ) { const threadID = ThreadID . fromString ( remote . id ?? '' ) ; const changes = dexie . table ( ChangeTableName ) ; const count = await changes . count ( ) ; expect ( count ) . to . equal ( 6 ) ; await remote . push ( 'dogs' ) ; expect ( await changes . count ( ) ) . to . equal ( 0 ) ; await remote . push ( 'dogs' ) ; const client = createDbClient ( remote . config ) ; const dogs = dexie . table ( 'dogs' ) ; const total = await dogs . count ( ) ; expect ( total ) . to . equal ( 2 ) ; const q = new Where ( 'age' ) . gt ( 0 ) ; const instances = await client . find ( threadID , 'dogs' , q ) ; expect ( instances ) . to . have . lengthOf ( total ) ; } ) ; it ( 'should pull changes from remote and automatically update local db' , async function ( ) { this . timeout ( 5000 ) ; try { await remote . pull ( 'dogs' ) ; throw shouldHaveThrown ; } catch ( err ) { expect ( err ) . to . equal ( Errors . LocalChangesError ) ; } await remote . push ( 'dogs' ) ; const changed1 = await remote . pull ( 'dogs' ) ; expect ( changed1 ) . to . have . lengthOf ( 0 ) ; const threadID = ThreadID . fromString ( remote . id ?? '' ) ; const client = createDbClient ( remote . config ) ; const dogs = dexie . table < Dog > ( 'dogs' ) ; const array = await dogs . toArray ( ) ; await client . delete ( threadID , 'dogs' , [ array [ 0 ] . _id ! ] ) ; array [ 1 ] . name = 'Mod' ; await client . save ( threadID , 'dogs' , [ array [ 1 ] ] ) ; const changed2 = await remote . pull ( 'dogs' ) ; expect ( changed2 ) . to . have . lengthOf ( 2 ) ; expect ( await dogs . count ( ) ) . to . equal ( 1 ) ; } ) ; } ) ; } ) ; } ) ;