11import { faker } from '@faker-js/faker' ;
22import { desc , eq } from 'drizzle-orm' ;
33import { StatusCodes } from 'http-status-codes' ;
4+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
5+ // @ts -expect-error
6+ import * as encoding from 'lib0/encoding' ;
47import { AddressInfo } from 'net' ;
58import { v4 } from 'uuid' ;
69import waitForExpect from 'wait-for-expect' ;
@@ -28,6 +31,7 @@ import { db } from '../../../../drizzle/db';
2831import { itemsRawTable , pageUpdateTable } from '../../../../drizzle/schema' ;
2932import { assertIsDefined } from '../../../../utils/assertions' ;
3033import { assertIsMemberForTest } from '../../../authentication' ;
34+ import { MESSAGE_SYNC_CODE } from './constants' ;
3135import { PageItemService } from './page.service' ;
3236
3337async function getAppPort ( app : FastifyInstance ) {
@@ -36,6 +40,14 @@ async function getAppPort(app: FastifyInstance) {
3640 return port ;
3741}
3842
43+ async function expectServerToBeResponsive ( app : FastifyInstance ) {
44+ const result = await app . inject ( {
45+ method : 'GET' ,
46+ url : '/version' ,
47+ } ) ;
48+ expect ( result . statusCode ) . toEqual ( StatusCodes . OK ) ;
49+ }
50+
3951async function connectToItemWs (
4052 app : FastifyInstance ,
4153 itemId : string ,
@@ -340,6 +352,82 @@ describe('Page routes tests', () => {
340352 provider1 . destroy ( ) ;
341353 provider2 . destroy ( ) ;
342354 } ) ;
355+
356+ it ( 'Gracefully recover if update is corrupted' , async ( ) => {
357+ const {
358+ actor,
359+ items : [ item ] ,
360+ } = await seedFromJson ( {
361+ items : [
362+ {
363+ type : ItemType . PAGE ,
364+ memberships : [ { account : 'actor' , permission : PermissionLevel . Write } ] ,
365+ } ,
366+ ] ,
367+ } ) ;
368+ assertIsDefined ( actor ) ;
369+ mockAuthenticate ( actor ) ;
370+
371+ // prefill incorrect update in db
372+ await db
373+ . insert ( pageUpdateTable )
374+ . values ( { itemId : item . id , clock : 1 , update : Buffer . from ( [ 1 , 2 , 3 ] ) } ) ;
375+
376+ const { doc, provider : provider1 } = await connectToItemWs ( app , item . id ) ;
377+
378+ // connection should close
379+ let hasClosed = false ;
380+ provider1 . on ( 'connection-close' , ( ) => {
381+ hasClosed = true ;
382+ } ) ;
383+ await waitForExpect ( async ( ) => {
384+ expect ( hasClosed ) . toBeTruthy ( ) ;
385+ await expectServerToBeResponsive ( app ) ;
386+ } , 2000 ) ;
387+
388+ // cleanup
389+ doc . destroy ( ) ;
390+ provider1 . destroy ( ) ;
391+ } ) ;
392+
393+ it ( 'Gracefully recover if receive corrupted ws message' , async ( ) => {
394+ const {
395+ actor,
396+ items : [ item ] ,
397+ } = await seedFromJson ( {
398+ items : [
399+ {
400+ type : ItemType . PAGE ,
401+ memberships : [ { account : 'actor' , permission : PermissionLevel . Write } ] ,
402+ } ,
403+ ] ,
404+ } ) ;
405+ assertIsDefined ( actor ) ;
406+ mockAuthenticate ( actor ) ;
407+ const port = await getAppPort ( app ) ;
408+ const ws = new WebSocket ( `ws://localhost:${ port } /items/pages/${ item . id } /ws` ) ;
409+
410+ // connection should close
411+ let hasClosed = false ;
412+ ws . on ( 'close' , ( ) => {
413+ hasClosed = true ;
414+ } ) ;
415+
416+ // wait for connection to be established before switching user
417+ await waitForExpect ( ( ) => {
418+ expect ( ws . readyState ) . toBeTruthy ( ) ;
419+ } , 2000 ) ;
420+
421+ const encoder = encoding . createEncoder ( ) ;
422+ encoding . writeVarUint ( encoder , MESSAGE_SYNC_CODE ) ;
423+ encoding . writeVarUint8Array ( encoder , Buffer . from ( [ 1 , 2 , 3 ] ) ) ;
424+ ws . send ( encoding . toUint8Array ( encoder ) ) ;
425+
426+ await waitForExpect ( async ( ) => {
427+ expect ( hasClosed ) . toBeTruthy ( ) ;
428+ await expectServerToBeResponsive ( app ) ;
429+ } , 4000 ) ;
430+ } ) ;
343431 } ) ;
344432
345433 describe ( 'GET /items/pages/ws/read' , ( ) => {
@@ -653,6 +741,81 @@ describe('Page routes tests', () => {
653741 provider . destroy ( ) ;
654742 readerProvider . destroy ( ) ;
655743 } ) ;
744+
745+ it ( 'Gracefully recover if update is corrupted' , async ( ) => {
746+ const {
747+ actor,
748+ items : [ item ] ,
749+ } = await seedFromJson ( {
750+ items : [
751+ {
752+ type : ItemType . PAGE ,
753+ memberships : [ { account : 'actor' , permission : PermissionLevel . Read } ] ,
754+ } ,
755+ ] ,
756+ } ) ;
757+ assertIsDefined ( actor ) ;
758+ mockAuthenticate ( actor ) ;
759+
760+ // prefill incorrect update in db
761+ await db
762+ . insert ( pageUpdateTable )
763+ . values ( { itemId : item . id , clock : 1 , update : Buffer . from ( [ 1 , 2 , 3 ] ) } ) ;
764+
765+ const { doc, provider : provider1 } = await connectToItemWs ( app , item . id , { readOnly : true } ) ;
766+
767+ // connection should close
768+ let hasClosed = false ;
769+ provider1 . on ( 'connection-close' , ( ) => {
770+ hasClosed = true ;
771+ } ) ;
772+ await waitForExpect ( async ( ) => {
773+ expect ( hasClosed ) . toBeTruthy ( ) ;
774+ } , 2000 ) ;
775+
776+ // cleanup
777+ doc . destroy ( ) ;
778+ provider1 . destroy ( ) ;
779+ } ) ;
780+
781+ it ( 'Gracefully recover if receive corrupted ws message' , async ( ) => {
782+ const {
783+ actor,
784+ items : [ item ] ,
785+ } = await seedFromJson ( {
786+ items : [
787+ {
788+ type : ItemType . PAGE ,
789+ memberships : [ { account : 'actor' , permission : PermissionLevel . Read } ] ,
790+ } ,
791+ ] ,
792+ } ) ;
793+ assertIsDefined ( actor ) ;
794+ mockAuthenticate ( actor ) ;
795+ const port = await getAppPort ( app ) ;
796+ const ws = new WebSocket ( `ws://localhost:${ port } /items/pages/${ item . id } /ws/read` ) ;
797+
798+ // connection should close
799+ let hasClosed = false ;
800+ ws . on ( 'close' , ( ) => {
801+ hasClosed = true ;
802+ } ) ;
803+
804+ // wait for connection to be established before switching user
805+ await waitForExpect ( ( ) => {
806+ expect ( ws . readyState ) . toBeTruthy ( ) ;
807+ } , 2000 ) ;
808+
809+ const encoder = encoding . createEncoder ( ) ;
810+ encoding . writeVarUint ( encoder , MESSAGE_SYNC_CODE ) ;
811+ encoding . writeVarUint8Array ( encoder , Buffer . from ( [ 1 , 2 , 3 ] ) ) ;
812+ ws . send ( encoding . toUint8Array ( encoder ) ) ;
813+
814+ await waitForExpect ( async ( ) => {
815+ expect ( hasClosed ) . toBeTruthy ( ) ;
816+ await expectServerToBeResponsive ( app ) ;
817+ } , 4000 ) ;
818+ } ) ;
656819 } ) ;
657820
658821 describe ( 'copy post hook' , ( ) => {
0 commit comments