summaryrefslogtreecommitdiff
path: root/spec/frontend/ide/lib/mirror_spec.js
blob: 8f417ea54dcf6d39081cca41f70128bd29736031 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
import createDiff from '~/ide/lib/create_diff';
import {
  canConnect,
  createMirror,
  SERVICE_NAME,
  PROTOCOL,
  MSG_CONNECTION_ERROR,
  SERVICE_DELAY,
} from '~/ide/lib/mirror';
import { getWebSocketUrl } from '~/lib/utils/url_utility';

jest.mock('~/ide/lib/create_diff', () => jest.fn());

const TEST_PATH = '/project/ide/proxy/path';
const TEST_DIFF = {
  patch: 'lorem ipsum',
  toDelete: ['foo.md'],
};
const TEST_ERROR = 'Something bad happened...';
const TEST_SUCCESS_RESPONSE = {
  data: JSON.stringify({ error: { code: 0 }, payload: { status_code: 200 } }),
};
const TEST_ERROR_RESPONSE = {
  data: JSON.stringify({ error: { code: 1, Message: TEST_ERROR }, payload: { status_code: 200 } }),
};
const TEST_ERROR_PAYLOAD_RESPONSE = {
  data: JSON.stringify({
    error: { code: 0 },
    payload: { status_code: 500, error_message: TEST_ERROR },
  }),
};

const buildUploadMessage = ({ toDelete, patch }) =>
  JSON.stringify({
    code: 'EVENT',
    namespace: '/files',
    event: 'PATCH',
    payload: { diff: patch, delete_files: toDelete },
  });

describe('ide/lib/mirror', () => {
  describe('canConnect', () => {
    it('can connect if the session has the expected service', () => {
      const result = canConnect({ services: ['test1', SERVICE_NAME, 'test2'] });

      expect(result).toBe(true);
    });

    it('cannot connect if the session does not have the expected service', () => {
      const result = canConnect({ services: ['test1', 'test2'] });

      expect(result).toBe(false);
    });
  });

  describe('createMirror', () => {
    const origWebSocket = global.WebSocket;
    let mirror;
    let mockWebSocket;

    beforeEach(() => {
      mockWebSocket = {
        close: jest.fn(),
        send: jest.fn(),
      };
      global.WebSocket = jest.fn().mockImplementation(() => mockWebSocket);
      mirror = createMirror();
    });

    afterEach(() => {
      global.WebSocket = origWebSocket;
    });

    const waitForConnection = (delay = SERVICE_DELAY) => {
      const wait = new Promise((resolve) => {
        setTimeout(resolve, 10);
      });

      jest.advanceTimersByTime(delay);

      return wait;
    };
    const connectPass = () => waitForConnection().then(() => mockWebSocket.onopen());
    const connectFail = () => waitForConnection().then(() => mockWebSocket.onerror());
    const sendResponse = (msg) => {
      mockWebSocket.onmessage(msg);
    };

    describe('connect', () => {
      let connection;

      beforeEach(() => {
        connection = mirror.connect(TEST_PATH);
      });

      it('waits before creating web socket', () => {
        // ignore error when test suite terminates
        connection.catch(() => {});

        return waitForConnection(SERVICE_DELAY - 10).then(() => {
          expect(global.WebSocket).not.toHaveBeenCalled();
        });
      });

      it('is canceled when disconnected before finished waiting', () => {
        mirror.disconnect();

        return waitForConnection(SERVICE_DELAY).then(() => {
          expect(global.WebSocket).not.toHaveBeenCalled();
        });
      });

      describe('when connection is successful', () => {
        beforeEach(connectPass);

        it('connects to service', () => {
          const expectedPath = `${getWebSocketUrl(TEST_PATH)}?service=${SERVICE_NAME}`;

          return connection.then(() => {
            expect(global.WebSocket).toHaveBeenCalledWith(expectedPath, [PROTOCOL]);
          });
        });

        it('disconnects when connected again', () => {
          const result = connection
            .then(() => {
              // https://gitlab.com/gitlab-org/gitlab/issues/33024
              // eslint-disable-next-line promise/no-nesting
              mirror.connect(TEST_PATH).catch(() => {});
            })
            .then(() => {
              expect(mockWebSocket.close).toHaveBeenCalled();
            });

          return result;
        });
      });

      describe('when connection fails', () => {
        beforeEach(connectFail);

        it('rejects with error', () => {
          return expect(connection).rejects.toEqual(new Error(MSG_CONNECTION_ERROR));
        });
      });
    });

    describe('upload', () => {
      let state;

      beforeEach(() => {
        state = { changedFiles: [] };
        createDiff.mockReturnValue(TEST_DIFF);

        const connection = mirror.connect(TEST_PATH);

        return connectPass().then(() => connection);
      });

      it('creates a diff from the given state', () => {
        const result = mirror.upload(state);

        sendResponse(TEST_SUCCESS_RESPONSE);

        return result.then(() => {
          expect(createDiff).toHaveBeenCalledWith(state);
          expect(mockWebSocket.send).toHaveBeenCalledWith(buildUploadMessage(TEST_DIFF));
        });
      });

      it.each`
        response                       | description
        ${TEST_ERROR_RESPONSE}         | ${'error in error'}
        ${TEST_ERROR_PAYLOAD_RESPONSE} | ${'error in payload'}
      `('rejects if response has $description', ({ response }) => {
        const result = mirror.upload(state);

        sendResponse(response);

        return expect(result).rejects.toEqual({ message: TEST_ERROR });
      });
    });
  });
});