@forge/realtime 0.0.7-next.0 → 0.1.0-experimental-3f68b81

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,7 +8,8 @@ exports[`publish should publish an event when the payload is an object 1`] = `
8
8
  },
9
9
  "/",
10
10
  {
11
- "body": "{"query":"mutation publishRealtimeChannel(\\n $installationId: ID!\\n $name: String!\\n $payload: String!\\n $isGlobal: Boolean\\n){\\n ecosystem {\\n publishRealtimeChannel(\\n installationId: $installationId\\n name: $name\\n payload: $payload\\n isGlobal: $isGlobal\\n ) {\\n eventId,\\n eventTimestamp\\n }\\n }\\n}","variables":{"installationId":"iiiiiiii-iiii-iiii-iiii-iiiiiiiiiiii","name":"my-channel","payload":"{\\"value\\":\\"this is a test payload\\",\\"funLevel\\":100}","isGlobal":false}}",
11
+ "body": "{"query":"mutation publishRealtimeChannel(\\n $installationId: ID!\\n $name: String!\\n $payload: String!\\n $isGlobal: Boolean\\n $token: String\\n){\\n ecosystem {\\n publishRealtimeChannel(\\n installationId: $installationId\\n name: $name\\n payload: $payload\\n isGlobal: $isGlobal\\n token: $token\\n ) {\\n eventId,\\n eventTimestamp\\n }\\n }\\n}","variables":{"installationId":"iiiiiiii-iiii-iiii-iiii-iiiiiiiiiiii","name":"my-channel","payload":"{\\"value\\":\\"this is a test payload\\",\\"funLevel\\":100}","isGlobal":false,"token":"my-token"}}",
12
+ "errors": [],
12
13
  "headers": {
13
14
  "Content-Type": "application/json",
14
15
  "x-forge-context-token": "my.context.token",
@@ -27,7 +28,28 @@ exports[`publish should publish an event with FCT in header 1`] = `
27
28
  },
28
29
  "/",
29
30
  {
30
- "body": "{"query":"mutation publishRealtimeChannel(\\n $installationId: ID!\\n $name: String!\\n $payload: String!\\n $isGlobal: Boolean\\n){\\n ecosystem {\\n publishRealtimeChannel(\\n installationId: $installationId\\n name: $name\\n payload: $payload\\n isGlobal: $isGlobal\\n ) {\\n eventId,\\n eventTimestamp\\n }\\n }\\n}","variables":{"installationId":"iiiiiiii-iiii-iiii-iiii-iiiiiiiiiiii","name":"my-channel","payload":"this is an event payload","isGlobal":false}}",
31
+ "body": "{"query":"mutation publishRealtimeChannel(\\n $installationId: ID!\\n $name: String!\\n $payload: String!\\n $isGlobal: Boolean\\n $token: String\\n){\\n ecosystem {\\n publishRealtimeChannel(\\n installationId: $installationId\\n name: $name\\n payload: $payload\\n isGlobal: $isGlobal\\n token: $token\\n ) {\\n eventId,\\n eventTimestamp\\n }\\n }\\n}","variables":{"installationId":"iiiiiiii-iiii-iiii-iiii-iiiiiiiiiiii","name":"my-channel","payload":"this is an event payload","isGlobal":false,"token":"my-token"}}",
32
+ "errors": [],
33
+ "headers": {
34
+ "Content-Type": "application/json",
35
+ "x-forge-context-token": "my.context.token",
36
+ },
37
+ "method": "POST",
38
+ },
39
+ ],
40
+ ]
41
+ `;
42
+
43
+ exports[`publish should return null eventId and eventTimestamp when the response has errors 1`] = `
44
+ [
45
+ [
46
+ {
47
+ "type": "realtime",
48
+ },
49
+ "/",
50
+ {
51
+ "body": "{"query":"mutation publishRealtimeChannel(\\n $installationId: ID!\\n $name: String!\\n $payload: String!\\n $isGlobal: Boolean\\n $token: String\\n){\\n ecosystem {\\n publishRealtimeChannel(\\n installationId: $installationId\\n name: $name\\n payload: $payload\\n isGlobal: $isGlobal\\n token: $token\\n ) {\\n eventId,\\n eventTimestamp\\n }\\n }\\n}","variables":{"installationId":"iiiiiiii-iiii-iiii-iiii-iiiiiiiiiiii","name":"my-channel","payload":"{\\"value\\":\\"this is a test payload\\",\\"funLevel\\":100}","isGlobal":false}}",
52
+ "errors": [],
31
53
  "headers": {
32
54
  "Content-Type": "application/json",
33
55
  "x-forge-context-token": "my.context.token",
@@ -46,7 +68,8 @@ exports[`publish should throw an error when Forge Outbound Proxy returns an erro
46
68
  },
47
69
  "/",
48
70
  {
49
- "body": "{"query":"mutation publishRealtimeChannel(\\n $installationId: ID!\\n $name: String!\\n $payload: String!\\n $isGlobal: Boolean\\n){\\n ecosystem {\\n publishRealtimeChannel(\\n installationId: $installationId\\n name: $name\\n payload: $payload\\n isGlobal: $isGlobal\\n ) {\\n eventId,\\n eventTimestamp\\n }\\n }\\n}","variables":{"installationId":"iiiiiiii-iiii-iiii-iiii-iiiiiiiiiiii","name":"my-channel","payload":"{\\"value\\":\\"this is a test payload\\",\\"funLevel\\":100}","isGlobal":false}}",
71
+ "body": "{"query":"mutation publishRealtimeChannel(\\n $installationId: ID!\\n $name: String!\\n $payload: String!\\n $isGlobal: Boolean\\n $token: String\\n){\\n ecosystem {\\n publishRealtimeChannel(\\n installationId: $installationId\\n name: $name\\n payload: $payload\\n isGlobal: $isGlobal\\n token: $token\\n ) {\\n eventId,\\n eventTimestamp\\n }\\n }\\n}","variables":{"installationId":"iiiiiiii-iiii-iiii-iiii-iiiiiiiiiiii","name":"my-channel","payload":"{\\"value\\":\\"this is a test payload\\",\\"funLevel\\":100}","isGlobal":false,"token":"my-token"}}",
72
+ "errors": [],
50
73
  "headers": {
51
74
  "Content-Type": "application/json",
52
75
  "x-forge-context-token": "my.context.token",
@@ -65,7 +88,8 @@ exports[`publishGlobal should publish an event 1`] = `
65
88
  },
66
89
  "/",
67
90
  {
68
- "body": "{"query":"mutation publishRealtimeChannel(\\n $installationId: ID!\\n $name: String!\\n $payload: String!\\n $isGlobal: Boolean\\n){\\n ecosystem {\\n publishRealtimeChannel(\\n installationId: $installationId\\n name: $name\\n payload: $payload\\n isGlobal: $isGlobal\\n ) {\\n eventId,\\n eventTimestamp\\n }\\n }\\n}","variables":{"installationId":"iiiiiiii-iiii-iiii-iiii-iiiiiiiiiiii","name":"my-channel","payload":"this is an event payload","isGlobal":true}}",
91
+ "body": "{"query":"mutation publishRealtimeChannel(\\n $installationId: ID!\\n $name: String!\\n $payload: String!\\n $isGlobal: Boolean\\n $token: String\\n){\\n ecosystem {\\n publishRealtimeChannel(\\n installationId: $installationId\\n name: $name\\n payload: $payload\\n isGlobal: $isGlobal\\n token: $token\\n ) {\\n eventId,\\n eventTimestamp\\n }\\n }\\n}","variables":{"installationId":"iiiiiiii-iiii-iiii-iiii-iiiiiiiiiiii","name":"my-channel","payload":"this is an event payload","isGlobal":true,"token":"my-token"}}",
92
+ "errors": [],
69
93
  "headers": {
70
94
  "Content-Type": "application/json",
71
95
  },
@@ -83,7 +107,27 @@ exports[`publishGlobal should publish an event when the payload is an object 1`]
83
107
  },
84
108
  "/",
85
109
  {
86
- "body": "{"query":"mutation publishRealtimeChannel(\\n $installationId: ID!\\n $name: String!\\n $payload: String!\\n $isGlobal: Boolean\\n){\\n ecosystem {\\n publishRealtimeChannel(\\n installationId: $installationId\\n name: $name\\n payload: $payload\\n isGlobal: $isGlobal\\n ) {\\n eventId,\\n eventTimestamp\\n }\\n }\\n}","variables":{"installationId":"iiiiiiii-iiii-iiii-iiii-iiiiiiiiiiii","name":"my-channel","payload":"{\\"value\\":\\"this is a test payload\\",\\"funLevel\\":100}","isGlobal":true}}",
110
+ "body": "{"query":"mutation publishRealtimeChannel(\\n $installationId: ID!\\n $name: String!\\n $payload: String!\\n $isGlobal: Boolean\\n $token: String\\n){\\n ecosystem {\\n publishRealtimeChannel(\\n installationId: $installationId\\n name: $name\\n payload: $payload\\n isGlobal: $isGlobal\\n token: $token\\n ) {\\n eventId,\\n eventTimestamp\\n }\\n }\\n}","variables":{"installationId":"iiiiiiii-iiii-iiii-iiii-iiiiiiiiiiii","name":"my-channel","payload":"{\\"value\\":\\"this is a test payload\\",\\"funLevel\\":100}","isGlobal":true,"token":"my-token"}}",
111
+ "errors": [],
112
+ "headers": {
113
+ "Content-Type": "application/json",
114
+ },
115
+ "method": "POST",
116
+ },
117
+ ],
118
+ ]
119
+ `;
120
+
121
+ exports[`publishGlobal should return null eventId and eventTimestamp when the response has errors 1`] = `
122
+ [
123
+ [
124
+ {
125
+ "type": "realtime",
126
+ },
127
+ "/",
128
+ {
129
+ "body": "{"query":"mutation publishRealtimeChannel(\\n $installationId: ID!\\n $name: String!\\n $payload: String!\\n $isGlobal: Boolean\\n $token: String\\n){\\n ecosystem {\\n publishRealtimeChannel(\\n installationId: $installationId\\n name: $name\\n payload: $payload\\n isGlobal: $isGlobal\\n token: $token\\n ) {\\n eventId,\\n eventTimestamp\\n }\\n }\\n}","variables":{"installationId":"iiiiiiii-iiii-iiii-iiii-iiiiiiiiiiii","name":"my-channel","payload":"{\\"value\\":\\"this is a test payload\\",\\"funLevel\\":100}","isGlobal":true}}",
130
+ "errors": [],
87
131
  "headers": {
88
132
  "Content-Type": "application/json",
89
133
  },
@@ -101,7 +145,8 @@ exports[`publishGlobal should throw an error when Forge Outbound Proxy returns a
101
145
  },
102
146
  "/",
103
147
  {
104
- "body": "{"query":"mutation publishRealtimeChannel(\\n $installationId: ID!\\n $name: String!\\n $payload: String!\\n $isGlobal: Boolean\\n){\\n ecosystem {\\n publishRealtimeChannel(\\n installationId: $installationId\\n name: $name\\n payload: $payload\\n isGlobal: $isGlobal\\n ) {\\n eventId,\\n eventTimestamp\\n }\\n }\\n}","variables":{"installationId":"iiiiiiii-iiii-iiii-iiii-iiiiiiiiiiii","name":"my-channel","payload":"{\\"value\\":\\"this is a test payload\\",\\"funLevel\\":100}","isGlobal":true}}",
148
+ "body": "{"query":"mutation publishRealtimeChannel(\\n $installationId: ID!\\n $name: String!\\n $payload: String!\\n $isGlobal: Boolean\\n $token: String\\n){\\n ecosystem {\\n publishRealtimeChannel(\\n installationId: $installationId\\n name: $name\\n payload: $payload\\n isGlobal: $isGlobal\\n token: $token\\n ) {\\n eventId,\\n eventTimestamp\\n }\\n }\\n}","variables":{"installationId":"iiiiiiii-iiii-iiii-iiii-iiiiiiiiiiii","name":"my-channel","payload":"{\\"value\\":\\"this is a test payload\\",\\"funLevel\\":100}","isGlobal":true,"token":"my-token"}}",
149
+ "errors": [],
105
150
  "headers": {
106
151
  "Content-Type": "application/json",
107
152
  },
@@ -0,0 +1,58 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`signRealtimeToken should send a signRealtimeToken request and recieve a jwt and expiresAt 1`] = `
4
+ [
5
+ [
6
+ {
7
+ "type": "realtime",
8
+ },
9
+ "/",
10
+ {
11
+ "body": "{"query":"mutation signRealtimeToken(\\n $channelName: String!\\n $claims: JSON!\\n){\\n ecosystem {\\n signRealtimeToken(\\n channelName: $channelName\\n claims: $claims\\n ) {\\n errors {\\n message\\n extensions {\\n errorType\\n statusCode\\n }\\n }\\n forgeRealtimeToken {\\n jwt\\n expiresAt\\n }\\n success\\n }\\n }\\n}","variables":{"channelName":"my-channel","claims":{"test-key":"test-value"}}}",
12
+ "errors": [],
13
+ "headers": {
14
+ "Content-Type": "application/json",
15
+ },
16
+ "method": "POST",
17
+ },
18
+ ],
19
+ ]
20
+ `;
21
+
22
+ exports[`signRealtimeToken should send generic error when errors are returned 1`] = `
23
+ [
24
+ [
25
+ {
26
+ "type": "realtime",
27
+ },
28
+ "/",
29
+ {
30
+ "body": "{"query":"mutation signRealtimeToken(\\n $channelName: String!\\n $claims: JSON!\\n){\\n ecosystem {\\n signRealtimeToken(\\n channelName: $channelName\\n claims: $claims\\n ) {\\n errors {\\n message\\n extensions {\\n errorType\\n statusCode\\n }\\n }\\n forgeRealtimeToken {\\n jwt\\n expiresAt\\n }\\n success\\n }\\n }\\n}","variables":{"channelName":"my-channel","claims":{"test-key":"test-value"}}}",
31
+ "errors": [],
32
+ "headers": {
33
+ "Content-Type": "application/json",
34
+ },
35
+ "method": "POST",
36
+ },
37
+ ],
38
+ ]
39
+ `;
40
+
41
+ exports[`signRealtimeToken should throw an error when Forge Outbound Proxy returns an error 1`] = `
42
+ [
43
+ [
44
+ {
45
+ "type": "realtime",
46
+ },
47
+ "/",
48
+ {
49
+ "body": "{"query":"mutation signRealtimeToken(\\n $channelName: String!\\n $claims: JSON!\\n){\\n ecosystem {\\n signRealtimeToken(\\n channelName: $channelName\\n claims: $claims\\n ) {\\n errors {\\n message\\n extensions {\\n errorType\\n statusCode\\n }\\n }\\n forgeRealtimeToken {\\n jwt\\n expiresAt\\n }\\n success\\n }\\n }\\n}","variables":{"channelName":"my-channel","claims":{"test-key":"test-value"}}}",
50
+ "errors": [],
51
+ "headers": {
52
+ "Content-Type": "application/json",
53
+ },
54
+ "method": "POST",
55
+ },
56
+ ],
57
+ ]
58
+ `;
@@ -2,6 +2,36 @@ import * as runtime from '../runtime';
2
2
  import { publish, publishGlobal } from '../publish';
3
3
  import { FORGE_RUNTIME } from './utils';
4
4
 
5
+ const TEST_EVENT_ID = 'event-id';
6
+ const TEST_EVENT_TIMESTAMP = '1234567890';
7
+ const MOCK_CHANNEL = 'my-channel';
8
+ const MOCK_PUBLISH_OPTIONS = { token: 'my-token' };
9
+ const MOCK_EVENT_PAYLOAD_STRING = 'this is an event payload';
10
+ const MOCK_EVENT_PAYLOAD_OBJECT = { value: 'this is a test payload', funLevel: 100 };
11
+
12
+ const MOCK_FETCH_RESPONSE = {
13
+ errors: [],
14
+ data: {
15
+ ecosystem: {
16
+ publishRealtimeChannel: {
17
+ eventId: TEST_EVENT_ID,
18
+ eventTimestamp: TEST_EVENT_TIMESTAMP
19
+ },
20
+ success: true
21
+ }
22
+ }
23
+ }
24
+
25
+ const MOCK_ERRORS = [
26
+ {
27
+ message: 'Error message',
28
+ extensions: {
29
+ errorType: 'Error type',
30
+ statusCode: 500
31
+ }
32
+ }
33
+ ];
34
+
5
35
  describe('publish', () => {
6
36
  beforeEach(() => {
7
37
  jest.restoreAllMocks();
@@ -12,16 +42,16 @@ describe('publish', () => {
12
42
  jest.spyOn(runtime, '__getRuntime').mockReturnValue(FORGE_RUNTIME);
13
43
 
14
44
  const mockForgeFetch = jest.fn().mockResolvedValue({
15
- text: jest.fn().mockResolvedValue('Hello Magic'),
45
+ json: jest.fn().mockResolvedValue(MOCK_FETCH_RESPONSE),
16
46
  status: 200,
17
47
  headers: { get: jest.fn().mockReturnValue(undefined) }
18
48
  });
19
49
 
20
50
  (global as any).__forge_fetch__ = mockForgeFetch;
21
51
 
22
- const response = await publish('my-channel', 'this is an event payload');
52
+ const response = await publish(MOCK_CHANNEL, MOCK_EVENT_PAYLOAD_STRING, MOCK_PUBLISH_OPTIONS);
23
53
 
24
- expect(response.status).toBe(200);
54
+ expect(response).toEqual({ eventId: TEST_EVENT_ID, eventTimestamp: TEST_EVENT_TIMESTAMP });
25
55
  expect(mockForgeFetch.mock.calls).toMatchSnapshot();
26
56
  });
27
57
 
@@ -29,17 +59,16 @@ describe('publish', () => {
29
59
  jest.spyOn(runtime, '__getRuntime').mockReturnValue(FORGE_RUNTIME);
30
60
 
31
61
  const mockForgeFetch = jest.fn().mockResolvedValue({
32
- text: jest.fn().mockResolvedValue('Hello Magic'),
62
+ json: jest.fn().mockResolvedValue(MOCK_FETCH_RESPONSE),
33
63
  status: 200,
34
64
  headers: { get: jest.fn().mockReturnValue(undefined) }
35
65
  });
36
66
 
37
67
  (global as any).__forge_fetch__ = mockForgeFetch;
38
68
 
39
- const payload = { value: 'this is a test payload', funLevel: 100 };
40
- const response = await publish('my-channel', JSON.stringify(payload));
69
+ const response = await publish(MOCK_CHANNEL, JSON.stringify(MOCK_EVENT_PAYLOAD_OBJECT), MOCK_PUBLISH_OPTIONS);
41
70
 
42
- expect(response.status).toBe(200);
71
+ expect(response).toEqual({ eventId: TEST_EVENT_ID, eventTimestamp: TEST_EVENT_TIMESTAMP });
43
72
  expect(mockForgeFetch.mock.calls).toMatchSnapshot();
44
73
  });
45
74
 
@@ -54,14 +83,38 @@ describe('publish', () => {
54
83
 
55
84
  (global as any).__forge_fetch__ = mockForgeFetch;
56
85
 
57
- const payload = { value: 'this is a test payload', funLevel: 100 };
58
- const response = publish('my-channel', JSON.stringify(payload));
86
+ const response = publish(MOCK_CHANNEL, JSON.stringify(MOCK_EVENT_PAYLOAD_OBJECT), MOCK_PUBLISH_OPTIONS);
59
87
 
60
88
  await expect(response).rejects.toThrow(
61
89
  'Forge platform failed to process runtime HTTP request - 502 - UPSTREAM_FAILURE'
62
90
  );
63
91
  expect(mockForgeFetch.mock.calls).toMatchSnapshot();
64
92
  });
93
+
94
+ it('should return null eventId and eventTimestamp when the response has errors', async () => {
95
+ jest.spyOn(runtime, '__getRuntime').mockReturnValue(FORGE_RUNTIME);
96
+
97
+ const mockForgeFetch = jest.fn().mockResolvedValue({
98
+ json: jest.fn().mockResolvedValue({
99
+ errors: MOCK_ERRORS,
100
+ data: {
101
+ ecosystem: {
102
+ publishRealtimeChannel: null,
103
+ }
104
+ }
105
+ }),
106
+ status: 200,
107
+ headers: { get: jest.fn().mockReturnValue(undefined) }
108
+ });
109
+
110
+ (global as any).__forge_fetch__ = mockForgeFetch;
111
+
112
+ const payload = { value: 'this is a test payload', funLevel: 100 };
113
+ const response = await publish('my-channel', JSON.stringify(payload));
114
+
115
+ expect(response).toEqual({ eventId: null, eventTimestamp: null, errors: MOCK_ERRORS });
116
+ expect(mockForgeFetch.mock.calls).toMatchSnapshot();
117
+ });
65
118
  });
66
119
 
67
120
  describe('publishGlobal', () => {
@@ -74,16 +127,16 @@ describe('publishGlobal', () => {
74
127
  jest.spyOn(runtime, '__getRuntime').mockReturnValue(FORGE_RUNTIME);
75
128
 
76
129
  const mockForgeFetch = jest.fn().mockResolvedValue({
77
- text: jest.fn().mockResolvedValue('Hello Magic'),
130
+ json: jest.fn().mockResolvedValue(MOCK_FETCH_RESPONSE),
78
131
  status: 200,
79
132
  headers: { get: jest.fn().mockReturnValue(undefined) }
80
133
  });
81
134
 
82
135
  (global as any).__forge_fetch__ = mockForgeFetch;
83
136
 
84
- const response = await publishGlobal('my-channel', 'this is an event payload');
137
+ const response = await publishGlobal(MOCK_CHANNEL, MOCK_EVENT_PAYLOAD_STRING, MOCK_PUBLISH_OPTIONS);
85
138
 
86
- expect(response.status).toBe(200);
139
+ expect(response).toEqual({ eventId: TEST_EVENT_ID, eventTimestamp: TEST_EVENT_TIMESTAMP });
87
140
  expect(mockForgeFetch.mock.calls).toMatchSnapshot();
88
141
  });
89
142
 
@@ -91,17 +144,16 @@ describe('publishGlobal', () => {
91
144
  jest.spyOn(runtime, '__getRuntime').mockReturnValue(FORGE_RUNTIME);
92
145
 
93
146
  const mockForgeFetch = jest.fn().mockResolvedValue({
94
- text: jest.fn().mockResolvedValue('Hello Magic'),
147
+ json: jest.fn().mockResolvedValue(MOCK_FETCH_RESPONSE),
95
148
  status: 200,
96
149
  headers: { get: jest.fn().mockReturnValue(undefined) }
97
150
  });
98
151
 
99
152
  (global as any).__forge_fetch__ = mockForgeFetch;
100
153
 
101
- const payload = { value: 'this is a test payload', funLevel: 100 };
102
- const response = await publishGlobal('my-channel', JSON.stringify(payload));
154
+ const response = await publishGlobal(MOCK_CHANNEL, JSON.stringify(MOCK_EVENT_PAYLOAD_OBJECT), MOCK_PUBLISH_OPTIONS);
103
155
 
104
- expect(response.status).toBe(200);
156
+ expect(response).toEqual({ eventId: TEST_EVENT_ID, eventTimestamp: TEST_EVENT_TIMESTAMP });
105
157
  expect(mockForgeFetch.mock.calls).toMatchSnapshot();
106
158
  });
107
159
 
@@ -116,12 +168,36 @@ describe('publishGlobal', () => {
116
168
 
117
169
  (global as any).__forge_fetch__ = mockForgeFetch;
118
170
 
119
- const payload = { value: 'this is a test payload', funLevel: 100 };
120
- const response = publishGlobal('my-channel', JSON.stringify(payload));
171
+ const response = publishGlobal(MOCK_CHANNEL, JSON.stringify(MOCK_EVENT_PAYLOAD_OBJECT), MOCK_PUBLISH_OPTIONS);
121
172
 
122
173
  await expect(response).rejects.toThrow(
123
174
  'Forge platform failed to process runtime HTTP request - 502 - UPSTREAM_FAILURE'
124
175
  );
125
176
  expect(mockForgeFetch.mock.calls).toMatchSnapshot();
126
177
  });
127
- });
178
+
179
+ it('should return null eventId and eventTimestamp when the response has errors', async () => {
180
+ jest.spyOn(runtime, '__getRuntime').mockReturnValue(FORGE_RUNTIME);
181
+
182
+ const mockForgeFetch = jest.fn().mockResolvedValue({
183
+ json: jest.fn().mockResolvedValue({
184
+ errors: MOCK_ERRORS,
185
+ data: {
186
+ ecosystem: {
187
+ publishRealtimeChannel: null,
188
+ }
189
+ }
190
+ }),
191
+ status: 200,
192
+ headers: { get: jest.fn().mockReturnValue(undefined) }
193
+ });
194
+
195
+ (global as any).__forge_fetch__ = mockForgeFetch;
196
+
197
+ const payload = { value: 'this is a test payload', funLevel: 100 };
198
+ const response = await publishGlobal('my-channel', JSON.stringify(payload));
199
+
200
+ expect(response).toEqual({ eventId: null, eventTimestamp: null, errors: MOCK_ERRORS });
201
+ expect(mockForgeFetch.mock.calls).toMatchSnapshot();
202
+ });
203
+ });
@@ -0,0 +1,96 @@
1
+ import * as runtime from '../runtime';
2
+ import { signRealtimeToken } from '../signRealtimeToken';
3
+ import { FORGE_RUNTIME } from './utils';
4
+
5
+ const TEST_TOKEN = 'jwt-token';
6
+ const TEST_EXPIRES_AT = '1234567890';
7
+ const MOCK_ERRORS = [
8
+ {
9
+ message: 'Error message',
10
+ extensions: {
11
+ errorType: 'Error type',
12
+ statusCode: 500
13
+ }
14
+ }
15
+ ];
16
+
17
+ describe('signRealtimeToken', () => {
18
+ beforeEach(() => {
19
+ jest.restoreAllMocks();
20
+ jest.resetAllMocks();
21
+ });
22
+
23
+ it('should send a signRealtimeToken request and recieve a jwt and expiresAt', async () => {
24
+ jest.spyOn(runtime, '__getRuntime').mockReturnValue(FORGE_RUNTIME);
25
+
26
+ const mockForgeFetch = jest.fn().mockResolvedValue({
27
+ json: jest.fn().mockResolvedValue({
28
+ errors: [],
29
+ data: {
30
+ ecosystem: {
31
+ signRealtimeToken: {
32
+ errors: [],
33
+ forgeRealtimeToken: {
34
+ jwt: TEST_TOKEN,
35
+ expiresAt: TEST_EXPIRES_AT
36
+ },
37
+ success: true
38
+ }
39
+ }
40
+ }
41
+ }),
42
+ status: 200,
43
+ headers: { get: jest.fn().mockReturnValue(undefined) }
44
+ });
45
+
46
+ (global as any).__forge_fetch__ = mockForgeFetch;
47
+
48
+ const response = await signRealtimeToken('my-channel', { 'test-key': 'test-value' });
49
+
50
+ expect(response).toEqual({ token: TEST_TOKEN, expiresAt: TEST_EXPIRES_AT });
51
+ expect(mockForgeFetch.mock.calls).toMatchSnapshot();
52
+ });
53
+
54
+ it('should throw an error when Forge Outbound Proxy returns an error', async () => {
55
+ jest.spyOn(runtime, '__getRuntime').mockReturnValue(FORGE_RUNTIME);
56
+
57
+ const mockForgeFetch = jest.fn().mockResolvedValue({
58
+ text: jest.fn().mockResolvedValue('Error occurred'),
59
+ status: 502,
60
+ headers: { get: jest.fn().mockReturnValue('UPSTREAM_FAILURE') }
61
+ });
62
+
63
+ (global as any).__forge_fetch__ = mockForgeFetch;
64
+
65
+ const response = signRealtimeToken('my-channel', { 'test-key': 'test-value' });
66
+
67
+ await expect(response).rejects.toThrow(
68
+ 'Forge platform failed to process runtime HTTP request - 502 - UPSTREAM_FAILURE'
69
+ );
70
+ expect(mockForgeFetch.mock.calls).toMatchSnapshot();
71
+ });
72
+
73
+ it('should send generic error when errors are returned', async () => {
74
+ jest.spyOn(runtime, '__getRuntime').mockReturnValue(FORGE_RUNTIME);
75
+
76
+ const mockForgeFetch = jest.fn().mockResolvedValue({
77
+ json: jest.fn().mockResolvedValue({
78
+ errors: MOCK_ERRORS,
79
+ data: {
80
+ ecosystem: {
81
+ signRealtimeToken: null,
82
+ }
83
+ }
84
+ }),
85
+ status: 200,
86
+ headers: { get: jest.fn().mockReturnValue(undefined) }
87
+ });
88
+
89
+ (global as any).__forge_fetch__ = mockForgeFetch;
90
+
91
+ const response = await signRealtimeToken('my-channel', { 'test-key': 'test-value' });
92
+
93
+ expect(response).toEqual({ token: null, expiresAt: null, errors: MOCK_ERRORS });
94
+ expect(mockForgeFetch.mock.calls).toMatchSnapshot();
95
+ });
96
+ });
@@ -15,7 +15,28 @@ export const FORGE_RUNTIME: ForgeRuntime = {
15
15
  functionKey: 'functionKey',
16
16
  moduleType: 'moduleType',
17
17
  moduleKey: 'moduleKey',
18
- license: { isActive: true }
18
+ license: { isActive: true },
19
+ permissions: {
20
+ scopes: ['read:confluence-content.summary', 'write:confluence-content'],
21
+ content: {
22
+ scripts: [],
23
+ styles: []
24
+ },
25
+ external: {
26
+ fetch: {
27
+ backend: [],
28
+ client: []
29
+ },
30
+ fonts: [],
31
+ frames: [],
32
+ navigation: [],
33
+ images: [],
34
+ media: [],
35
+ scripts: [],
36
+ styles: []
37
+ },
38
+ format: () => ''
39
+ }
19
40
  },
20
41
  tracing: {
21
42
  traceId: 'traceId',
package/src/index.ts CHANGED
@@ -1 +1,2 @@
1
1
  export { publish, publishGlobal } from './publish';
2
+ export { signRealtimeToken } from './signRealtimeToken';
package/src/publish.ts CHANGED
@@ -1,20 +1,12 @@
1
- import { ProxyRequestError } from '@forge/api';
2
-
3
1
  import { __getRuntime } from './runtime';
4
-
5
- export const getForgeProxyError = (response: Response) => response.headers.get('forge-proxy-error');
6
- export const handleProxyResponseErrors = (response: Response): void => {
7
- const errorReason = getForgeProxyError(response);
8
- if (errorReason) {
9
- throw new ProxyRequestError(response.status, errorReason);
10
- }
11
- };
2
+ import { handleProxyResponseErrors, prodEnvErrorMessage } from './utils';
12
3
 
13
4
  const graphqlBody = `mutation publishRealtimeChannel(
14
5
  $installationId: ID!
15
6
  $name: String!
16
7
  $payload: String!
17
8
  $isGlobal: Boolean
9
+ $token: String
18
10
  ){
19
11
  ecosystem {
20
12
  publishRealtimeChannel(
@@ -22,6 +14,7 @@ const graphqlBody = `mutation publishRealtimeChannel(
22
14
  name: $name
23
15
  payload: $payload
24
16
  isGlobal: $isGlobal
17
+ token: $token
25
18
  ) {
26
19
  eventId,
27
20
  eventTimestamp
@@ -29,10 +22,11 @@ const graphqlBody = `mutation publishRealtimeChannel(
29
22
  }
30
23
  }`;
31
24
 
32
- const prodEnvErrorMessage =
33
- 'Forge realtime usage is restricted to Forge apps in a non-production environment. Please see https://developer.atlassian.com/platform/forge/cli-reference/environments/ for reference on Forge app environments.';
25
+ interface PublishOptions {
26
+ token?: string;
27
+ }
34
28
 
35
- export const publish = async (channelName: string, eventPayload: string) => {
29
+ export const publish = async (channelName: string, eventPayload: string, options?: PublishOptions) => {
36
30
  const { appContext, realtime } = __getRuntime();
37
31
 
38
32
  if (appContext?.environmentType === 'PRODUCTION') {
@@ -53,9 +47,11 @@ export const publish = async (channelName: string, eventPayload: string) => {
53
47
  installationId: appContext.installationId,
54
48
  name: channelName,
55
49
  payload: eventPayload,
56
- isGlobal: false
50
+ isGlobal: false,
51
+ token: options?.token
57
52
  }
58
53
  }),
54
+ errors: [],
59
55
  headers: {
60
56
  'Content-Type': 'application/json',
61
57
  'x-forge-context-token': realtime?.contextToken
@@ -65,10 +61,39 @@ export const publish = async (channelName: string, eventPayload: string) => {
65
61
 
66
62
  handleProxyResponseErrors(response);
67
63
 
68
- return response;
64
+ const awaitedResponse = await response.json();
65
+
66
+ const { data, errors } = awaitedResponse;
67
+
68
+ if (errors && errors.length > 0) {
69
+ return {
70
+ eventId: null,
71
+ eventTimestamp: null,
72
+ errors
73
+ };
74
+ }
75
+
76
+ if (!data) {
77
+ return {
78
+ eventId: null,
79
+ eventTimestamp: null,
80
+ errors: [
81
+ {
82
+ message: 'Error publishing event to channel.'
83
+ }
84
+ ]
85
+ };
86
+ }
87
+
88
+ const { eventId, eventTimestamp } = awaitedResponse.data.ecosystem.publishRealtimeChannel;
89
+
90
+ return {
91
+ eventId,
92
+ eventTimestamp
93
+ };
69
94
  };
70
95
 
71
- export const publishGlobal = async (channelName: string, eventPayload: string) => {
96
+ export const publishGlobal = async (channelName: string, eventPayload: string, options?: PublishOptions) => {
72
97
  const { appContext } = __getRuntime();
73
98
 
74
99
  if (appContext?.environmentType === 'PRODUCTION') {
@@ -89,9 +114,11 @@ export const publishGlobal = async (channelName: string, eventPayload: string) =
89
114
  installationId: appContext.installationId,
90
115
  name: channelName,
91
116
  payload: eventPayload,
92
- isGlobal: true
117
+ isGlobal: true,
118
+ token: options?.token
93
119
  }
94
120
  }),
121
+ errors: [],
95
122
  headers: {
96
123
  'Content-Type': 'application/json'
97
124
  }
@@ -100,5 +127,34 @@ export const publishGlobal = async (channelName: string, eventPayload: string) =
100
127
 
101
128
  handleProxyResponseErrors(response);
102
129
 
103
- return response;
130
+ const awaitedResponse = await response.json();
131
+
132
+ const { data, errors } = awaitedResponse;
133
+
134
+ if (errors && errors.length > 0) {
135
+ return {
136
+ eventId: null,
137
+ eventTimestamp: null,
138
+ errors
139
+ };
140
+ }
141
+
142
+ if (!data) {
143
+ return {
144
+ eventId: null,
145
+ eventTimestamp: null,
146
+ errors: [
147
+ {
148
+ message: 'Error publishing global event to channel.'
149
+ }
150
+ ]
151
+ };
152
+ }
153
+
154
+ const { eventId, eventTimestamp } = data.ecosystem.publishRealtimeChannel;
155
+
156
+ return {
157
+ eventId,
158
+ eventTimestamp
159
+ };
104
160
  };