@@ -34,6 +34,7 @@ import {
3434 IMatrixApiError ,
3535 IReadEventFromWidgetActionRequest ,
3636 ISendEventFromWidgetActionRequest ,
37+ ISendToDeviceFromWidgetActionRequest ,
3738 IUpdateDelayedEventFromWidgetActionRequest ,
3839 IUploadFileActionFromWidgetActionRequest ,
3940 IWidgetApiErrorResponseDataDetails ,
@@ -117,6 +118,7 @@ describe('ClientWidgetApi', () => {
117118 sendEvent : jest . fn ( ) ,
118119 sendDelayedEvent : jest . fn ( ) ,
119120 updateDelayedEvent : jest . fn ( ) ,
121+ sendToDevice : jest . fn ( ) ,
120122 validateCapabilities : jest . fn ( ) ,
121123 searchUserDirectory : jest . fn ( ) ,
122124 getMediaConfig : jest . fn ( ) ,
@@ -721,6 +723,250 @@ describe('ClientWidgetApi', () => {
721723 } ) ;
722724 } ) ;
723725
726+ describe ( 'send_to_device action' , ( ) => {
727+ it ( 'sends unencrypted to-device events' , async ( ) => {
728+ const event : ISendToDeviceFromWidgetActionRequest = {
729+ api : WidgetApiDirection . FromWidget ,
730+ widgetId : 'test' ,
731+ requestId : '0' ,
732+ action : WidgetApiFromWidgetAction . SendToDevice ,
733+ data : {
734+ type : 'net.example.test' ,
735+ encrypted : false ,
736+ messages : {
737+ '@foo:bar.com' : {
738+ 'DEVICEID' : {
739+ 'example_content_key' : 'value' ,
740+ } ,
741+ } ,
742+ } ,
743+ } ,
744+ } ;
745+
746+ await loadIframe ( [ `org.matrix.msc3819.send.to_device:${ event . data . type } ` ] ) ;
747+
748+ emitEvent ( new CustomEvent ( '' , { detail : event } ) ) ;
749+
750+ await waitFor ( ( ) => {
751+ expect ( transport . reply ) . toHaveBeenCalledWith ( event , { } ) ;
752+ } ) ;
753+
754+ expect ( driver . sendToDevice ) . toHaveBeenCalledWith (
755+ event . data . type ,
756+ event . data . encrypted ,
757+ event . data . messages ,
758+ ) ;
759+ } ) ;
760+
761+ it ( 'fails to send to-device events without event type' , async ( ) => {
762+ const event : IWidgetApiRequest = {
763+ api : WidgetApiDirection . FromWidget ,
764+ widgetId : 'test' ,
765+ requestId : '0' ,
766+ action : WidgetApiFromWidgetAction . SendToDevice ,
767+ data : {
768+ encrypted : false ,
769+ messages : {
770+ '@foo:bar.com' : {
771+ 'DEVICEID' : {
772+ 'example_content_key' : 'value' ,
773+ } ,
774+ } ,
775+ } ,
776+ } ,
777+ } ;
778+
779+ await loadIframe ( [ `org.matrix.msc3819.send.to_device:${ event . data . type } ` ] ) ;
780+
781+ emitEvent ( new CustomEvent ( '' , { detail : event } ) ) ;
782+
783+ await waitFor ( ( ) => {
784+ expect ( transport . reply ) . toBeCalledWith ( event , {
785+ error : { message : 'Invalid request - missing event type' } ,
786+ } ) ;
787+ } ) ;
788+
789+ expect ( driver . sendToDevice ) . not . toBeCalled ( ) ;
790+ } ) ;
791+
792+ it ( 'fails to send to-device events without event contents' , async ( ) => {
793+ const event : IWidgetApiRequest = {
794+ api : WidgetApiDirection . FromWidget ,
795+ widgetId : 'test' ,
796+ requestId : '0' ,
797+ action : WidgetApiFromWidgetAction . SendToDevice ,
798+ data : {
799+ type : 'net.example.test' ,
800+ encrypted : false ,
801+ } ,
802+ } ;
803+
804+ await loadIframe ( [ `org.matrix.msc3819.send.to_device:${ event . data . type } ` ] ) ;
805+
806+ emitEvent ( new CustomEvent ( '' , { detail : event } ) ) ;
807+
808+ await waitFor ( ( ) => {
809+ expect ( transport . reply ) . toBeCalledWith ( event , {
810+ error : { message : 'Invalid request - missing event contents' } ,
811+ } ) ;
812+ } ) ;
813+
814+ expect ( driver . sendToDevice ) . not . toBeCalled ( ) ;
815+ } ) ;
816+
817+ it ( 'fails to send to-device events without encryption flag' , async ( ) => {
818+ const event : IWidgetApiRequest = {
819+ api : WidgetApiDirection . FromWidget ,
820+ widgetId : 'test' ,
821+ requestId : '0' ,
822+ action : WidgetApiFromWidgetAction . SendToDevice ,
823+ data : {
824+ type : 'net.example.test' ,
825+ messages : {
826+ '@foo:bar.com' : {
827+ 'DEVICEID' : {
828+ 'example_content_key' : 'value' ,
829+ } ,
830+ } ,
831+ } ,
832+ } ,
833+ } ;
834+
835+ await loadIframe ( [ `org.matrix.msc3819.send.to_device:${ event . data . type } ` ] ) ;
836+
837+ emitEvent ( new CustomEvent ( '' , { detail : event } ) ) ;
838+
839+ await waitFor ( ( ) => {
840+ expect ( transport . reply ) . toBeCalledWith ( event , {
841+ error : { message : 'Invalid request - missing encryption flag' } ,
842+ } ) ;
843+ } ) ;
844+
845+ expect ( driver . sendToDevice ) . not . toBeCalled ( ) ;
846+ } ) ;
847+
848+ it ( 'fails to send to-device events with any event type' , async ( ) => {
849+ const event : ISendToDeviceFromWidgetActionRequest = {
850+ api : WidgetApiDirection . FromWidget ,
851+ widgetId : 'test' ,
852+ requestId : '0' ,
853+ action : WidgetApiFromWidgetAction . SendToDevice ,
854+ data : {
855+ type : 'net.example.test' ,
856+ encrypted : false ,
857+ messages : {
858+ '@foo:bar.com' : {
859+ 'DEVICEID' : {
860+ 'example_content_key' : 'value' ,
861+ } ,
862+ } ,
863+ } ,
864+ } ,
865+ } ;
866+
867+ await loadIframe ( [ `org.matrix.msc3819.send.to_device:${ event . data . type } _different` ] ) ;
868+
869+ emitEvent ( new CustomEvent ( '' , { detail : event } ) ) ;
870+
871+ await waitFor ( ( ) => {
872+ expect ( transport . reply ) . toBeCalledWith ( event , {
873+ error : { message : 'Cannot send to-device events of this type' } ,
874+ } ) ;
875+ } ) ;
876+
877+ expect ( driver . sendToDevice ) . not . toBeCalled ( ) ;
878+ } ) ;
879+
880+ it ( 'should reject requests when the driver throws an exception' , async ( ) => {
881+ driver . sendToDevice . mockRejectedValue (
882+ new Error ( "M_FORBIDDEN: You don't have permission to send to-device events" ) ,
883+ ) ;
884+
885+ const event : ISendToDeviceFromWidgetActionRequest = {
886+ api : WidgetApiDirection . FromWidget ,
887+ widgetId : 'test' ,
888+ requestId : '0' ,
889+ action : WidgetApiFromWidgetAction . SendToDevice ,
890+ data : {
891+ type : 'net.example.test' ,
892+ encrypted : false ,
893+ messages : {
894+ '@foo:bar.com' : {
895+ 'DEVICEID' : {
896+ 'example_content_key' : 'value' ,
897+ } ,
898+ } ,
899+ } ,
900+ } ,
901+ } ;
902+
903+ await loadIframe ( [ `org.matrix.msc3819.send.to_device:${ event . data . type } ` ] ) ;
904+
905+ emitEvent ( new CustomEvent ( '' , { detail : event } ) ) ;
906+
907+ await waitFor ( ( ) => {
908+ expect ( transport . reply ) . toBeCalledWith ( event , {
909+ error : { message : 'Error sending event' } ,
910+ } ) ;
911+ } ) ;
912+ } ) ;
913+
914+ it ( 'should reject with Matrix API error response thrown by driver' , async ( ) => {
915+ driver . processError . mockImplementation ( processCustomMatrixError ) ;
916+
917+ driver . sendToDevice . mockRejectedValue (
918+ new CustomMatrixError (
919+ 'failed to send event' ,
920+ 400 ,
921+ 'M_FORBIDDEN' ,
922+ {
923+ reason : "You don't have permission to send to-device events" ,
924+ } ,
925+ ) ,
926+ ) ;
927+
928+ const event : ISendToDeviceFromWidgetActionRequest = {
929+ api : WidgetApiDirection . FromWidget ,
930+ widgetId : 'test' ,
931+ requestId : '0' ,
932+ action : WidgetApiFromWidgetAction . SendToDevice ,
933+ data : {
934+ type : 'net.example.test' ,
935+ encrypted : false ,
936+ messages : {
937+ '@foo:bar.com' : {
938+ 'DEVICEID' : {
939+ 'example_content_key' : 'value' ,
940+ } ,
941+ } ,
942+ } ,
943+ } ,
944+ } ;
945+
946+ await loadIframe ( [ `org.matrix.msc3819.send.to_device:${ event . data . type } ` ] ) ;
947+
948+ emitEvent ( new CustomEvent ( '' , { detail : event } ) ) ;
949+
950+ await waitFor ( ( ) => {
951+ expect ( transport . reply ) . toBeCalledWith ( event , {
952+ error : {
953+ message : 'Error sending event' ,
954+ matrix_api_error : {
955+ http_status : 400 ,
956+ http_headers : { } ,
957+ url : '' ,
958+ response : {
959+ errcode : 'M_FORBIDDEN' ,
960+ error : 'failed to send event' ,
961+ reason : "You don't have permission to send to-device events" ,
962+ } ,
963+ } satisfies IMatrixApiError ,
964+ } ,
965+ } ) ;
966+ } ) ;
967+ } ) ;
968+ } ) ;
969+
724970 describe ( 'org.matrix.msc2876.read_events action' , ( ) => {
725971 it ( 'reads state events with any state key' , async ( ) => {
726972 driver . readStateEvents . mockResolvedValue ( [
0 commit comments