@auxiora/connector-social 1.0.0 → 1.3.1

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.
package/src/reddit.ts DELETED
@@ -1,268 +0,0 @@
1
- import { defineConnector } from '@auxiora/connectors';
2
- import type { TriggerEvent } from '@auxiora/connectors';
3
-
4
- const REDDIT_BASE = 'https://oauth.reddit.com';
5
- const REDDIT_UA = 'auxiora:v1.0.0 (by /u/auxiora)';
6
-
7
- async function redditGet(token: string, path: string): Promise<Record<string, unknown>> {
8
- const res = await fetch(`${REDDIT_BASE}${path}`, {
9
- headers: {
10
- 'Authorization': `Bearer ${token}`,
11
- 'User-Agent': REDDIT_UA,
12
- },
13
- });
14
- if (!res.ok) throw new Error(`Reddit API error: ${res.status} ${await res.text().catch(() => res.statusText)}`);
15
- return res.json() as Promise<Record<string, unknown>>;
16
- }
17
-
18
- async function redditPost(token: string, path: string, body: Record<string, string>): Promise<Record<string, unknown>> {
19
- const res = await fetch(`${REDDIT_BASE}${path}`, {
20
- method: 'POST',
21
- headers: {
22
- 'Authorization': `Bearer ${token}`,
23
- 'User-Agent': REDDIT_UA,
24
- 'Content-Type': 'application/x-www-form-urlencoded',
25
- },
26
- body: new URLSearchParams(body).toString(),
27
- });
28
- if (!res.ok) throw new Error(`Reddit API error: ${res.status} ${await res.text().catch(() => res.statusText)}`);
29
- return res.json() as Promise<Record<string, unknown>>;
30
- }
31
-
32
- function extractPosts(listing: Record<string, unknown>): unknown[] {
33
- const data = listing.data as Record<string, unknown> | undefined;
34
- const children = (data?.children ?? []) as Array<Record<string, unknown>>;
35
- return children.map((c) => c.data);
36
- }
37
-
38
- export const redditConnector = defineConnector({
39
- id: 'reddit',
40
- name: 'Reddit',
41
- description: 'Integration with Reddit for browsing, posting, and messaging',
42
- version: '1.0.0',
43
- category: 'social',
44
- icon: 'reddit',
45
-
46
- auth: {
47
- type: 'oauth2',
48
- oauth2: {
49
- authUrl: 'https://www.reddit.com/api/v1/authorize',
50
- tokenUrl: 'https://www.reddit.com/api/v1/access_token',
51
- scopes: ['read', 'submit', 'privatemessages', 'vote', 'save', 'identity'],
52
- },
53
- },
54
-
55
- actions: [
56
- {
57
- id: 'front-page',
58
- name: 'Read Front Page',
59
- description: 'Read posts from the Reddit front page',
60
- trustMinimum: 1,
61
- trustDomain: 'messaging',
62
- reversible: false,
63
- sideEffects: false,
64
- params: {},
65
- },
66
- {
67
- id: 'subreddit-read',
68
- name: 'Read Subreddit',
69
- description: 'Read posts from a specific subreddit',
70
- trustMinimum: 1,
71
- trustDomain: 'messaging',
72
- reversible: false,
73
- sideEffects: false,
74
- params: {
75
- subreddit: { type: 'string', description: 'Subreddit name', required: true },
76
- },
77
- },
78
- {
79
- id: 'post-submit',
80
- name: 'Submit Post',
81
- description: 'Submit a new post to a subreddit',
82
- trustMinimum: 3,
83
- trustDomain: 'messaging',
84
- reversible: false,
85
- sideEffects: true,
86
- params: {
87
- subreddit: { type: 'string', description: 'Subreddit name', required: true },
88
- title: { type: 'string', description: 'Post title', required: true },
89
- body: { type: 'string', description: 'Post body', required: true },
90
- },
91
- },
92
- {
93
- id: 'comment',
94
- name: 'Post Comment',
95
- description: 'Post a comment on a Reddit post',
96
- trustMinimum: 3,
97
- trustDomain: 'messaging',
98
- reversible: false,
99
- sideEffects: true,
100
- params: {
101
- postId: { type: 'string', description: 'Post ID to comment on', required: true },
102
- body: { type: 'string', description: 'Comment body', required: true },
103
- },
104
- },
105
- {
106
- id: 'inbox-read',
107
- name: 'Read Inbox',
108
- description: 'Read Reddit inbox messages',
109
- trustMinimum: 1,
110
- trustDomain: 'messaging',
111
- reversible: false,
112
- sideEffects: false,
113
- params: {},
114
- },
115
- {
116
- id: 'search',
117
- name: 'Search Reddit',
118
- description: 'Search across Reddit',
119
- trustMinimum: 1,
120
- trustDomain: 'messaging',
121
- reversible: false,
122
- sideEffects: false,
123
- params: {
124
- query: { type: 'string', description: 'Search query', required: true },
125
- },
126
- },
127
- {
128
- id: 'save-post',
129
- name: 'Save Post',
130
- description: 'Save a Reddit post',
131
- trustMinimum: 1,
132
- trustDomain: 'messaging',
133
- reversible: true,
134
- sideEffects: true,
135
- params: {
136
- postId: { type: 'string', description: 'Post ID to save', required: true },
137
- },
138
- },
139
- {
140
- id: 'upvote',
141
- name: 'Upvote',
142
- description: 'Upvote or downvote a post',
143
- trustMinimum: 2,
144
- trustDomain: 'messaging',
145
- reversible: true,
146
- sideEffects: true,
147
- params: {
148
- postId: { type: 'string', description: 'Post ID to vote on', required: true },
149
- direction: { type: 'string', description: 'Vote direction (up or down)', default: 'up' },
150
- },
151
- },
152
- ],
153
-
154
- triggers: [
155
- {
156
- id: 'new-inbox',
157
- name: 'New Inbox Message',
158
- description: 'Triggered when a new inbox message is received',
159
- type: 'poll',
160
- pollIntervalMs: 120_000,
161
- },
162
- {
163
- id: 'subreddit-new',
164
- name: 'New Subreddit Post',
165
- description: 'Triggered when a new post appears in a monitored subreddit',
166
- type: 'poll',
167
- pollIntervalMs: 300_000,
168
- },
169
- ],
170
-
171
- entities: [
172
- {
173
- id: 'post',
174
- name: 'Post',
175
- description: 'A Reddit post',
176
- fields: { id: 'string', title: 'string', subreddit: 'string', author: 'string', score: 'number', commentCount: 'number' },
177
- },
178
- {
179
- id: 'comment',
180
- name: 'Comment',
181
- description: 'A Reddit comment',
182
- fields: { id: 'string', body: 'string', author: 'string', score: 'number', postId: 'string' },
183
- },
184
- ],
185
-
186
- async executeAction(actionId: string, params: Record<string, unknown>, token: string): Promise<unknown> {
187
- switch (actionId) {
188
- case 'front-page': {
189
- const res = await redditGet(token, '/best?limit=25');
190
- return { posts: extractPosts(res) };
191
- }
192
- case 'subreddit-read': {
193
- const sub = params.subreddit as string;
194
- const res = await redditGet(token, `/r/${sub}/hot?limit=25`);
195
- return { posts: extractPosts(res) };
196
- }
197
- case 'post-submit': {
198
- const res = await redditPost(token, '/api/submit', {
199
- sr: params.subreddit as string,
200
- kind: 'self',
201
- title: params.title as string,
202
- text: params.body as string,
203
- });
204
- const json = res.json as Record<string, unknown> | undefined;
205
- const data = json?.data as Record<string, unknown> | undefined;
206
- return { postId: data?.id ?? data?.name, status: 'submitted' };
207
- }
208
- case 'comment': {
209
- const postId = params.postId as string;
210
- const thingId = postId.startsWith('t3_') ? postId : `t3_${postId}`;
211
- const res = await redditPost(token, '/api/comment', {
212
- thing_id: thingId,
213
- text: params.body as string,
214
- });
215
- const json = res.json as Record<string, unknown> | undefined;
216
- const data = json?.data as Record<string, unknown> | undefined;
217
- const things = data?.things as Array<Record<string, unknown>> | undefined;
218
- const commentData = things?.[0]?.data as Record<string, unknown> | undefined;
219
- return { commentId: commentData?.id ?? commentData?.name, status: 'posted' };
220
- }
221
- case 'inbox-read': {
222
- const res = await redditGet(token, '/message/inbox?limit=25');
223
- return { messages: extractPosts(res) };
224
- }
225
- case 'search': {
226
- const query = encodeURIComponent(params.query as string);
227
- const res = await redditGet(token, `/search?q=${query}&limit=25`);
228
- return { posts: extractPosts(res) };
229
- }
230
- case 'save-post': {
231
- const postId = params.postId as string;
232
- const fullId = postId.startsWith('t3_') ? postId : `t3_${postId}`;
233
- await redditPost(token, '/api/save', { id: fullId });
234
- return { postId: params.postId, status: 'saved' };
235
- }
236
- case 'upvote': {
237
- const postId = params.postId as string;
238
- const fullId = postId.startsWith('t3_') ? postId : `t3_${postId}`;
239
- const direction = (params.direction as string) ?? 'up';
240
- const dir = direction === 'up' ? '1' : '-1';
241
- await redditPost(token, '/api/vote', { id: fullId, dir });
242
- return { postId: params.postId, status: 'voted' };
243
- }
244
- default:
245
- throw new Error(`Unknown action: ${actionId}`);
246
- }
247
- },
248
-
249
- async pollTrigger(triggerId: string, token: string, _lastPollAt?: number): Promise<TriggerEvent[]> {
250
- switch (triggerId) {
251
- case 'new-inbox': {
252
- const res = await redditGet(token, '/message/unread?limit=25');
253
- const messages = extractPosts(res) as Array<Record<string, unknown>>;
254
- return messages.map((m) => ({
255
- triggerId: 'new-inbox',
256
- connectorId: 'reddit',
257
- data: m,
258
- timestamp: typeof m.created_utc === 'number' ? (m.created_utc as number) * 1000 : Date.now(),
259
- }));
260
- }
261
- case 'subreddit-new':
262
- // Cannot poll specific subreddit without config
263
- return [];
264
- default:
265
- return [];
266
- }
267
- },
268
- });
package/src/twitter.ts DELETED
@@ -1,253 +0,0 @@
1
- import { defineConnector } from '@auxiora/connectors';
2
- import type { TriggerEvent } from '@auxiora/connectors';
3
-
4
- async function twitterFetch(token: string, path: string, options?: { method?: string; body?: unknown }) {
5
- const res = await fetch(`https://api.twitter.com/2${path}`, {
6
- method: options?.method ?? 'GET',
7
- headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
8
- body: options?.body ? JSON.stringify(options.body) : undefined,
9
- });
10
- if (!res.ok) throw new Error(`Twitter API error: ${res.status} ${await res.text().catch(() => res.statusText)}`);
11
- return res.json() as Promise<Record<string, unknown>>;
12
- }
13
-
14
- async function getMyUserId(token: string): Promise<string> {
15
- const res = await twitterFetch(token, '/users/me');
16
- const data = res.data as Record<string, unknown>;
17
- return data.id as string;
18
- }
19
-
20
- export const twitterConnector = defineConnector({
21
- id: 'twitter',
22
- name: 'Twitter / X',
23
- description: 'Integration with Twitter/X for tweets, mentions, and direct messages',
24
- version: '1.0.0',
25
- category: 'social',
26
- icon: 'twitter',
27
-
28
- auth: {
29
- type: 'oauth2',
30
- oauth2: {
31
- authUrl: 'https://twitter.com/i/oauth2/authorize',
32
- tokenUrl: 'https://api.twitter.com/2/oauth2/token',
33
- scopes: ['tweet.read', 'tweet.write', 'users.read', 'dm.read', 'dm.write', 'offline.access'],
34
- },
35
- },
36
-
37
- actions: [
38
- {
39
- id: 'timeline-read',
40
- name: 'Read Timeline',
41
- description: 'Read the authenticated user timeline',
42
- trustMinimum: 1,
43
- trustDomain: 'messaging',
44
- reversible: false,
45
- sideEffects: false,
46
- params: {},
47
- },
48
- {
49
- id: 'mentions-list',
50
- name: 'List Mentions',
51
- description: 'List recent mentions of the authenticated user',
52
- trustMinimum: 1,
53
- trustDomain: 'messaging',
54
- reversible: false,
55
- sideEffects: false,
56
- params: {},
57
- },
58
- {
59
- id: 'post-tweet',
60
- name: 'Post Tweet',
61
- description: 'Post a new tweet',
62
- trustMinimum: 3,
63
- trustDomain: 'messaging',
64
- reversible: false,
65
- sideEffects: true,
66
- params: {
67
- text: { type: 'string', description: 'Tweet text', required: true },
68
- },
69
- },
70
- {
71
- id: 'reply-tweet',
72
- name: 'Reply to Tweet',
73
- description: 'Reply to an existing tweet',
74
- trustMinimum: 3,
75
- trustDomain: 'messaging',
76
- reversible: false,
77
- sideEffects: true,
78
- params: {
79
- tweetId: { type: 'string', description: 'Tweet ID to reply to', required: true },
80
- text: { type: 'string', description: 'Reply text', required: true },
81
- },
82
- },
83
- {
84
- id: 'delete-tweet',
85
- name: 'Delete Tweet',
86
- description: 'Delete an existing tweet',
87
- trustMinimum: 3,
88
- trustDomain: 'messaging',
89
- reversible: false,
90
- sideEffects: true,
91
- params: {
92
- tweetId: { type: 'string', description: 'Tweet ID to delete', required: true },
93
- },
94
- },
95
- {
96
- id: 'search-tweets',
97
- name: 'Search Tweets',
98
- description: 'Search for tweets matching a query',
99
- trustMinimum: 1,
100
- trustDomain: 'messaging',
101
- reversible: false,
102
- sideEffects: false,
103
- params: {
104
- query: { type: 'string', description: 'Search query', required: true },
105
- },
106
- },
107
- {
108
- id: 'dm-list',
109
- name: 'List Direct Messages',
110
- description: 'List recent direct messages',
111
- trustMinimum: 1,
112
- trustDomain: 'messaging',
113
- reversible: false,
114
- sideEffects: false,
115
- params: {},
116
- },
117
- {
118
- id: 'dm-send',
119
- name: 'Send Direct Message',
120
- description: 'Send a direct message to a user',
121
- trustMinimum: 3,
122
- trustDomain: 'messaging',
123
- reversible: false,
124
- sideEffects: true,
125
- params: {
126
- recipientId: { type: 'string', description: 'Recipient user ID', required: true },
127
- text: { type: 'string', description: 'Message text', required: true },
128
- },
129
- },
130
- ],
131
-
132
- triggers: [
133
- {
134
- id: 'new-mention',
135
- name: 'New Mention',
136
- description: 'Triggered when the user is mentioned in a tweet',
137
- type: 'poll',
138
- pollIntervalMs: 60_000,
139
- },
140
- {
141
- id: 'new-dm',
142
- name: 'New Direct Message',
143
- description: 'Triggered when a new direct message is received',
144
- type: 'poll',
145
- pollIntervalMs: 120_000,
146
- },
147
- ],
148
-
149
- entities: [
150
- {
151
- id: 'tweet',
152
- name: 'Tweet',
153
- description: 'A tweet on Twitter/X',
154
- fields: { id: 'string', text: 'string', authorId: 'string', createdAt: 'string', likeCount: 'number', retweetCount: 'number' },
155
- },
156
- {
157
- id: 'direct-message',
158
- name: 'Direct Message',
159
- description: 'A direct message on Twitter/X',
160
- fields: { id: 'string', text: 'string', senderId: 'string', createdAt: 'string' },
161
- },
162
- ],
163
-
164
- async executeAction(actionId: string, params: Record<string, unknown>, token: string): Promise<unknown> {
165
- switch (actionId) {
166
- case 'timeline-read': {
167
- const userId = await getMyUserId(token);
168
- const res = await twitterFetch(token, `/users/${userId}/timelines/reverse_chronological`);
169
- return { tweets: res.data };
170
- }
171
- case 'mentions-list': {
172
- const userId = await getMyUserId(token);
173
- const res = await twitterFetch(token, `/users/${userId}/mentions`);
174
- return { mentions: res.data };
175
- }
176
- case 'post-tweet': {
177
- const res = await twitterFetch(token, '/tweets', {
178
- method: 'POST',
179
- body: { text: params.text },
180
- });
181
- const data = res.data as Record<string, unknown>;
182
- return { tweetId: data.id, status: 'posted' };
183
- }
184
- case 'reply-tweet': {
185
- const res = await twitterFetch(token, '/tweets', {
186
- method: 'POST',
187
- body: { text: params.text, reply: { in_reply_to_tweet_id: params.tweetId } },
188
- });
189
- const data = res.data as Record<string, unknown>;
190
- return { tweetId: data.id, status: 'replied' };
191
- }
192
- case 'delete-tweet': {
193
- await twitterFetch(token, `/tweets/${params.tweetId as string}`, { method: 'DELETE' });
194
- return { tweetId: params.tweetId, status: 'deleted' };
195
- }
196
- case 'search-tweets': {
197
- const query = encodeURIComponent(params.query as string);
198
- const res = await twitterFetch(token, `/tweets/search/recent?query=${query}`);
199
- return { tweets: res.data };
200
- }
201
- case 'dm-list': {
202
- const res = await twitterFetch(token, '/dm_events');
203
- return { messages: res.data };
204
- }
205
- case 'dm-send': {
206
- const res = await twitterFetch(token, `/dm_conversations/with/${params.recipientId as string}/messages`, {
207
- method: 'POST',
208
- body: { text: params.text },
209
- });
210
- const data = res.data as Record<string, unknown>;
211
- return { messageId: data.dm_event_id ?? data.id, status: 'sent' };
212
- }
213
- default:
214
- throw new Error(`Unknown action: ${actionId}`);
215
- }
216
- },
217
-
218
- async pollTrigger(triggerId: string, token: string, lastPollAt?: number): Promise<TriggerEvent[]> {
219
- switch (triggerId) {
220
- case 'new-mention': {
221
- const userId = await getMyUserId(token);
222
- const startTime = lastPollAt ? new Date(lastPollAt).toISOString() : undefined;
223
- const query = startTime ? `?start_time=${startTime}` : '';
224
- const res = await twitterFetch(token, `/users/${userId}/mentions${query}`);
225
- const mentions = (res.data ?? []) as Array<Record<string, unknown>>;
226
- return mentions.map((m) => ({
227
- triggerId: 'new-mention',
228
- connectorId: 'twitter',
229
- data: m,
230
- timestamp: m.created_at ? new Date(m.created_at as string).getTime() : Date.now(),
231
- }));
232
- }
233
- case 'new-dm': {
234
- const res = await twitterFetch(token, '/dm_events?event_types=MessageCreate');
235
- const events = (res.data ?? []) as Array<Record<string, unknown>>;
236
- const cutoff = lastPollAt ?? 0;
237
- return events
238
- .filter((e) => {
239
- const ts = e.created_at ? new Date(e.created_at as string).getTime() : 0;
240
- return ts > cutoff;
241
- })
242
- .map((e) => ({
243
- triggerId: 'new-dm',
244
- connectorId: 'twitter',
245
- data: e,
246
- timestamp: e.created_at ? new Date(e.created_at as string).getTime() : Date.now(),
247
- }));
248
- }
249
- default:
250
- return [];
251
- }
252
- },
253
- });
@@ -1,108 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { instagramConnector } from '../src/instagram.js';
3
-
4
- let fetchMock: ReturnType<typeof vi.fn>;
5
- beforeEach(() => {
6
- fetchMock = vi.fn();
7
- vi.stubGlobal('fetch', fetchMock);
8
- });
9
- afterEach(() => {
10
- vi.restoreAllMocks();
11
- });
12
-
13
- function mockResponse(body: unknown) {
14
- return { ok: true, status: 200, json: async () => body, text: async () => JSON.stringify(body) };
15
- }
16
-
17
- describe('Instagram Connector', () => {
18
- it('should have correct metadata', () => {
19
- expect(instagramConnector.id).toBe('instagram');
20
- expect(instagramConnector.name).toBe('Instagram');
21
- expect(instagramConnector.version).toBe('1.0.0');
22
- expect(instagramConnector.category).toBe('social');
23
- });
24
-
25
- it('should use OAuth2 authentication', () => {
26
- expect(instagramConnector.auth.type).toBe('oauth2');
27
- expect(instagramConnector.auth.oauth2).toBeDefined();
28
- expect(instagramConnector.auth.oauth2!.authUrl).toBe('https://api.instagram.com/oauth/authorize');
29
- expect(instagramConnector.auth.oauth2!.tokenUrl).toBe('https://api.instagram.com/oauth/access_token');
30
- expect(instagramConnector.auth.oauth2!.scopes).toContain('instagram_basic');
31
- expect(instagramConnector.auth.oauth2!.scopes.length).toBe(4);
32
- });
33
-
34
- it('should define all 6 actions', () => {
35
- expect(instagramConnector.actions).toHaveLength(6);
36
- const actionIds = instagramConnector.actions.map((a) => a.id);
37
- expect(actionIds).toContain('feed-read');
38
- expect(actionIds).toContain('stories-read');
39
- expect(actionIds).toContain('dm-list');
40
- expect(actionIds).toContain('dm-send');
41
- expect(actionIds).toContain('post-schedule');
42
- expect(actionIds).toContain('profile-get');
43
- });
44
-
45
- it('should have correct trust and side effect settings', () => {
46
- const readAction = instagramConnector.actions.find((a) => a.id === 'feed-read');
47
- expect(readAction!.trustMinimum).toBe(1);
48
- expect(readAction!.sideEffects).toBe(false);
49
-
50
- const dmAction = instagramConnector.actions.find((a) => a.id === 'dm-send');
51
- expect(dmAction!.trustMinimum).toBe(3);
52
- expect(dmAction!.sideEffects).toBe(true);
53
- });
54
-
55
- it('should execute feed-read action', async () => {
56
- // GET /me/media?fields=... -> { data: [] }
57
- fetchMock.mockResolvedValueOnce(mockResponse({ data: [] }));
58
- const result = await instagramConnector.executeAction('feed-read', {}, 'token');
59
- expect(result).toEqual({ posts: [] });
60
- });
61
-
62
- it('should execute dm-send action', async () => {
63
- // dm-send returns unavailable status (Instagram Messaging API requires approved access)
64
- const result = await instagramConnector.executeAction('dm-send', { recipientId: 'u1', text: 'Hi' }, 'token') as any;
65
- expect(result.status).toBe('unavailable');
66
- });
67
-
68
- it('should execute post-schedule action', async () => {
69
- // POST /me/media -> { id: 'container1' } (create media container)
70
- fetchMock.mockResolvedValueOnce(mockResponse({ id: 'container1' }));
71
- // POST /me/media_publish -> { id: 'post1' } (publish)
72
- fetchMock.mockResolvedValueOnce(mockResponse({ id: 'post1' }));
73
- const result = await instagramConnector.executeAction('post-schedule', { caption: 'Hello', mediaUrl: 'https://example.com/img.jpg' }, 'token') as any;
74
- expect(result.status).toBe('published');
75
- expect(result.postId).toBe('post1');
76
- });
77
-
78
- it('should execute profile-get action', async () => {
79
- // GET /me?fields=... -> profile object
80
- fetchMock.mockResolvedValueOnce(mockResponse({ id: 'me123', username: 'testuser', name: 'Test' }));
81
- const result = await instagramConnector.executeAction('profile-get', {}, 'token') as any;
82
- expect(result.id).toBe('me123');
83
- });
84
-
85
- it('should throw for unknown action', async () => {
86
- await expect(instagramConnector.executeAction('unknown', {}, 'token')).rejects.toThrow('Unknown action');
87
- });
88
-
89
- it('should return empty events from pollTrigger', async () => {
90
- // new-dm trigger returns [] directly without fetching
91
- const events = await instagramConnector.pollTrigger!('new-dm', 'token');
92
- expect(events).toEqual([]);
93
- });
94
-
95
- it('should define triggers', () => {
96
- expect(instagramConnector.triggers).toHaveLength(2);
97
- const triggerIds = instagramConnector.triggers.map((t) => t.id);
98
- expect(triggerIds).toContain('new-dm');
99
- expect(triggerIds).toContain('new-comment');
100
- });
101
-
102
- it('should define entities', () => {
103
- expect(instagramConnector.entities).toHaveLength(2);
104
- const entityIds = instagramConnector.entities.map((e) => e.id);
105
- expect(entityIds).toContain('post');
106
- expect(entityIds).toContain('story');
107
- });
108
- });