github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/realtime/realtime_test.go (about)

     1  package realtime
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/cozy/cozy-stack/pkg/config/config"
    10  	"github.com/cozy/cozy-stack/pkg/realtime"
    11  	"github.com/cozy/cozy-stack/tests/testutils"
    12  )
    13  
    14  type testDoc struct {
    15  	id      string
    16  	doctype string
    17  }
    18  
    19  func TestRealtime(t *testing.T) {
    20  	if testing.Short() {
    21  		t.Skip("an instance is required for this test: test skipped due to the use of --short flag")
    22  	}
    23  
    24  	config.UseTestFile(t)
    25  	testutils.NeedCouchdb(t)
    26  	setup := testutils.NewSetup(t, t.Name())
    27  	inst := setup.GetTestInstance()
    28  	_, token := setup.GetTestClient("io.cozy.foos io.cozy.bars io.cozy.bazs")
    29  	ts := setup.GetTestServer("/realtime", Routes)
    30  	t.Cleanup(ts.Close)
    31  
    32  	t.Run("WSNoAuth", func(t *testing.T) {
    33  		e := testutils.CreateTestClient(t, ts.URL)
    34  
    35  		ws := e.GET("/realtime/").
    36  			WithWebsocketUpgrade().
    37  			Expect().Status(http.StatusSwitchingProtocols).
    38  			Websocket()
    39  		defer ws.Disconnect()
    40  
    41  		obj := ws.WriteText(`{"method": "SUBSCRIBE", "payload": { "type": "io.cozy.foos" }}`).
    42  			Expect().TextMessage().
    43  			JSON().Object()
    44  
    45  		obj.ValueEqual("event", "error")
    46  		payload := obj.Value("payload").Object()
    47  		payload.ValueEqual("status", "405 Method Not Allowed")
    48  		payload.ValueEqual("code", "method not allowed")
    49  		payload.ValueEqual("title", "The SUBSCRIBE method is not supported")
    50  	})
    51  
    52  	t.Run("WSInvalidToken", func(t *testing.T) {
    53  		e := testutils.CreateTestClient(t, ts.URL)
    54  
    55  		ws := e.GET("/realtime/").
    56  			WithWebsocketUpgrade().
    57  			Expect().Status(http.StatusSwitchingProtocols).
    58  			Websocket()
    59  		defer ws.Disconnect()
    60  
    61  		obj := ws.WriteText(`{"method": "AUTH", "payload": "123456789"}`).
    62  			Expect().TextMessage().
    63  			JSON().Object()
    64  
    65  		obj.ValueEqual("event", "error")
    66  		payload := obj.Value("payload").Object()
    67  		payload.ValueEqual("status", "401 Unauthorized")
    68  		payload.ValueEqual("code", "unauthorized")
    69  		payload.ValueEqual("title", "The authentication has failed")
    70  	})
    71  
    72  	t.Run("WSNoPermissionsForADoctype", func(t *testing.T) {
    73  		e := testutils.CreateTestClient(t, ts.URL)
    74  
    75  		ws := e.GET("/realtime/").
    76  			WithWebsocketUpgrade().
    77  			Expect().Status(http.StatusSwitchingProtocols).
    78  			Websocket()
    79  		defer ws.Disconnect()
    80  
    81  		ws.WriteText(fmt.Sprintf(`{"method": "AUTH", "payload": "%s"}`, token))
    82  
    83  		obj := ws.WriteText(`{"method": "SUBSCRIBE", "payload": { "type": "io.cozy.contacts" }}`).
    84  			Expect().TextMessage().
    85  			JSON().Object()
    86  
    87  		obj.ValueEqual("event", "error")
    88  		payload := obj.Value("payload").Object()
    89  		payload.ValueEqual("status", "403 Forbidden")
    90  		payload.ValueEqual("code", "forbidden")
    91  		payload.ValueEqual("title", "The application can't subscribe to io.cozy.contacts")
    92  	})
    93  
    94  	t.Run("WSSuccess", func(t *testing.T) {
    95  		e := testutils.CreateTestClient(t, ts.URL)
    96  
    97  		ws := e.GET("/realtime/").
    98  			WithWebsocketUpgrade().
    99  			Expect().Status(http.StatusSwitchingProtocols).
   100  			Websocket()
   101  		defer ws.Disconnect()
   102  
   103  		ws.WriteText(fmt.Sprintf(`{"method": "AUTH", "payload": "%s"}`, token))
   104  
   105  		ws.WriteText(`{"method": "SUBSCRIBE", "payload": { "type": "io.cozy.foos" }}`)
   106  		ws.WriteText(`{"method": "SUBSCRIBE", "payload": { "type": "io.cozy.bars", "id": "bar-one"  }}`)
   107  		ws.WriteText(`{"method": "SUBSCRIBE", "payload": { "type": "io.cozy.bars", "id": "bar-two" }}`)
   108  
   109  		h := realtime.GetHub()
   110  		time.Sleep(30 * time.Millisecond)
   111  
   112  		h.Publish(inst, realtime.EventUpdate, &testDoc{
   113  			doctype: "io.cozy.foos",
   114  			id:      "foo-one",
   115  		}, nil)
   116  
   117  		obj := ws.Expect().TextMessage().JSON().Object()
   118  		obj.ValueEqual("event", "UPDATED")
   119  		payload := obj.Value("payload").Object()
   120  		payload.ValueEqual("type", "io.cozy.foos")
   121  		payload.ValueEqual("id", "foo-one")
   122  
   123  		h.Publish(inst, realtime.EventDelete, &testDoc{
   124  			doctype: "io.cozy.foos",
   125  			id:      "foo-two",
   126  		}, nil)
   127  
   128  		obj = ws.Expect().TextMessage().JSON().Object()
   129  		obj.ValueEqual("event", "DELETED")
   130  		payload = obj.Value("payload").Object()
   131  		payload.ValueEqual("type", "io.cozy.foos")
   132  		payload.ValueEqual("id", "foo-two")
   133  
   134  		h.Publish(inst, realtime.EventCreate, &testDoc{
   135  			doctype: "io.cozy.bars",
   136  			id:      "bar-three",
   137  		}, nil)
   138  		// No event
   139  
   140  		h.Publish(inst, realtime.EventCreate, &testDoc{
   141  			doctype: "io.cozy.bars",
   142  			id:      "bar-one",
   143  		}, nil)
   144  
   145  		obj = ws.Expect().TextMessage().JSON().Object()
   146  		obj.ValueEqual("event", "CREATED")
   147  		payload = obj.Value("payload").Object()
   148  		payload.ValueEqual("type", "io.cozy.bars")
   149  		payload.ValueEqual("id", "bar-one")
   150  
   151  		ws.WriteText(`{"method": "UNSUBSCRIBE", "payload": { "type": "io.cozy.bars", "id": "bar-one" }}`)
   152  		time.Sleep(30 * time.Millisecond)
   153  
   154  		h.Publish(inst, realtime.EventUpdate, &testDoc{
   155  			doctype: "io.cozy.bars",
   156  			id:      "bar-one",
   157  		}, nil)
   158  		// No event
   159  
   160  		h.Publish(inst, realtime.EventUpdate, &testDoc{
   161  			doctype: "io.cozy.bars",
   162  			id:      "bar-two",
   163  		}, nil)
   164  
   165  		obj = ws.Expect().TextMessage().JSON().Object()
   166  		obj.ValueEqual("event", "UPDATED")
   167  		payload = obj.Value("payload").Object()
   168  		payload.ValueEqual("type", "io.cozy.bars")
   169  		payload.ValueEqual("id", "bar-two")
   170  	})
   171  
   172  	t.Run("WSNotify", func(t *testing.T) {
   173  		e := testutils.CreateTestClient(t, ts.URL)
   174  
   175  		ws := e.GET("/realtime/").
   176  			WithWebsocketUpgrade().
   177  			Expect().Status(http.StatusSwitchingProtocols).
   178  			Websocket()
   179  		defer ws.Disconnect()
   180  
   181  		ws.WriteText(fmt.Sprintf(`{"method": "AUTH", "payload": "%s"}`, token))
   182  
   183  		ws.WriteText(`{"method": "SUBSCRIBE", "payload": { "type": "io.cozy.bazs", "id": "baz-one"  }}`)
   184  		time.Sleep(30 * time.Millisecond)
   185  
   186  		e.POST("/realtime/io.cozy.bazs/baz-one").
   187  			WithHeader("Content-Type", "application/json").
   188  			WithHeader("Authorization", "Bearer "+token).
   189  			WithBytes([]byte(`{ "hello": "world" }`)).
   190  			Expect().Status(204)
   191  
   192  		obj := ws.Expect().TextMessage().JSON().Object()
   193  		obj.ValueEqual("event", "NOTIFIED")
   194  		payload := obj.Value("payload").Object()
   195  		payload.ValueEqual("type", "io.cozy.bazs")
   196  		payload.ValueEqual("id", "baz-one")
   197  
   198  		doc := payload.Value("doc").Object()
   199  		doc.ValueEqual("hello", "world")
   200  	})
   201  }
   202  
   203  func (t *testDoc) ID() string      { return t.id }
   204  func (t *testDoc) DocType() string { return t.doctype }
   205  func (t *testDoc) MarshalJSON() ([]byte, error) {
   206  	j := `{"_id":"` + t.id + `", "_type":"` + t.doctype + `"}`
   207  	return []byte(j), nil
   208  }