From 8afb1bbc80029224ce33ec0a638e3d17734de48f Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 20 Jul 2026 13:53:27 -0600 Subject: [PATCH 1/5] test: point oembed API tests at the CI mock-server instead of real providers --- .github/workflows/ci-test-e2e.yml | 9 +- .../services/messages/lib/oembed/providers.ts | 8 + apps/meteor/tests/end-to-end/api/chat.ts | 237 +++++++++++++++--- docker-compose-ci.yml | 5 + 4 files changed, 228 insertions(+), 31 deletions(-) diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index 1fec4a979a109..643dc26cbe15e 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -191,9 +191,14 @@ jobs: # the deprecation logger log without throwing. Other suites use the # docker-compose default of TEST_MODE='true'. TEST_MODE: ${{ startsWith(inputs.type, 'api') && 'api' || 'true' }} + TEST_TYPE: ${{ inputs.type }} run: | - # when we are testing CE, we only need to start the rocketchat container - DEBUG_LOG_LEVEL=${DEBUG_LOG_LEVEL:-0} docker compose -f docker-compose-ci.yml up -d rocketchat --wait + # when we are testing CE, we only need to start the rocketchat container (plus mock-server for the api suite) + services=(rocketchat) + if [ "$TEST_TYPE" == "api" ]; then + services+=(mock-server) + fi + DEBUG_LOG_LEVEL=${DEBUG_LOG_LEVEL:-0} docker compose -f docker-compose-ci.yml up -d "${services[@]}" --wait - name: Start containers for EE if: inputs.release == 'ee' diff --git a/apps/meteor/server/services/messages/lib/oembed/providers.ts b/apps/meteor/server/services/messages/lib/oembed/providers.ts index e6931b6bc92b3..fea4a313ea45b 100644 --- a/apps/meteor/server/services/messages/lib/oembed/providers.ts +++ b/apps/meteor/server/services/messages/lib/oembed/providers.ts @@ -100,6 +100,14 @@ providers.registerProvider({ endPoint: 'https://www.loom.com/v1/oembed?format=json', }); +if (process.env.TEST_MODE) { + // resolves the oembed payload from the CI mock-server instead of a real provider — see the oembed suite in tests/end-to-end/api/chat.ts + providers.registerProvider({ + urls: [new RegExp('https?://mock-server\\.local(:\\d+)?/video/\\S+')], + endPoint: 'http://mock-server.local:8080/oembed', + }); +} + export const beforeGetUrlContent = (data: { urlObj: URL; }): { diff --git a/apps/meteor/tests/end-to-end/api/chat.ts b/apps/meteor/tests/end-to-end/api/chat.ts index 1792426f0b18c..9911780ee6610 100644 --- a/apps/meteor/tests/end-to-end/api/chat.ts +++ b/apps/meteor/tests/end-to-end/api/chat.ts @@ -10,6 +10,7 @@ import { sleep } from '../../../lib/utils/sleep'; import { getCredentials, api, request, credentials, apiUrl } from '../../data/api-data'; import { followMessage, sendSimpleMessage, deleteMessage } from '../../data/chat.helper'; import { imgURL } from '../../data/interactions'; +import { mockServerHealthy, mockServerReset, mockServerSet } from '../../data/mock-server.helper'; import { updatePermission, updateSetting } from '../../data/permissions.helper'; import { addUserToRoom, createRoom, deleteRoom, getSubscriptionByRoomId } from '../../data/rooms.helper'; import { password } from '../../data/user'; @@ -1262,14 +1263,56 @@ describe('[Chat]', () => { }); describe('oembed', () => { + // the TEST_MODE oembed provider (server/services/messages/lib/oembed/providers.ts) resolves /video/ URLs through GET /oembed on the mock-server + const mockVideoUrl = (id: string) => `http://mock-server.local:8080/video/${id}`; + const mockPageUrl = (id: string) => `http://mock-server.local:8080/page/${id}`; + const mockOembedEndpoint = { method: 'GET', path: '/oembed' }; + + const youtubeOembedResponse = { + title: 'Rocket.Chat Demo', + author_name: 'Rocket.Chat', + author_url: 'https://www.youtube.com/@RocketChatApp', + type: 'video', + height: 150, + width: 200, + version: '1.0', + provider_name: 'YouTube', + provider_url: 'https://www.youtube.com/', + thumbnail_height: 360, + thumbnail_width: 480, + thumbnail_url: 'https://i.ytimg.com/vi/T2v29gK8fP4/hqdefault.jpg', + html: '', + }; + + const spotifyOembedResponse = { + html: '', + iframe_url: 'https://open.spotify.com/embed/track/4cOdK2wGLETKBW3PvgPWqT?utm_source=oembed', + width: 456, + height: 152, + version: '1.0', + provider_name: 'Spotify', + provider_url: 'https://spotify.com', + type: 'rich', + title: 'Never Gonna Give You Up', + thumbnail_url: 'https://image-cdn-ak.spotifycdn.com/image/ab67616d00001e0215ebbedaacef61af244262a8', + thumbnail_width: 300, + thumbnail_height: 300, + }; + let ytEmbedMsgId: IMessage['_id']; let imgUrlMsgId: IMessage['_id']; + before(async () => { + const healthy = await mockServerHealthy(); + expect(healthy, 'mock-server is not reachable — ensure it is running').to.be.true; + await mockServerReset(); + }); + before(() => Promise.all([ updateSetting('API_EmbedIgnoredHosts', ''), - updateSetting('API_EmbedSafePorts', '80, 443, 3000'), - updateSetting('SSRF_Allowlist', '127.0.0.1:3000'), + updateSetting('API_EmbedSafePorts', '80, 443, 3000, 8080'), + updateSetting('SSRF_Allowlist', '127.0.0.1:3000, mock-server.local'), ]), ); @@ -1278,14 +1321,16 @@ describe('[Chat]', () => { updateSetting('API_EmbedIgnoredHosts', 'localhost, 127.0.0.1, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16'), updateSetting('API_EmbedSafePorts', '80, 443'), updateSetting('SSRF_Allowlist', ''), + mockServerReset(), ]), ); before(async () => { + await mockServerSet(mockOembedEndpoint.method, mockOembedEndpoint.path, youtubeOembedResponse); const ytEmbedMsgPayload = { _id: `id-${Date.now()}`, rid: testChannel._id, - msg: 'https://www.youtube.com/watch?v=T2v29gK8fP4', + msg: mockVideoUrl('embed-max-width'), emoji: ':smirk:', }; const ytPostResponse = await request.post(api('chat.sendMessage')).set(credentials).send({ message: ytEmbedMsgPayload }); @@ -1360,7 +1405,7 @@ describe('[Chat]', () => { .send({ message: { rid: testChannel._id, - msg: 'https://www.youtube.com/watch?v=T2v29gK8fP4', + msg: mockVideoUrl('empty-preview-urls'), }, previewUrls: [], }) @@ -1389,16 +1434,19 @@ describe('[Chat]', () => { }); it('should generate previews of chosen URL when the previewUrls array is provided', async () => { - let msgId; + const chosenUrl = mockPageUrl('chosen-preview'); + await mockServerSet('GET', '/page/chosen-preview', 'Chosen Preview Page'); + + let msgId: IMessage['_id']; await request .post(api('chat.sendMessage')) .set(credentials) .send({ message: { rid: testChannel._id, - msg: 'https://www.youtube.com/watch?v=T2v29gK8fP4 https://www.rocket.chat/', + msg: `${mockVideoUrl('not-chosen-preview')} ${chosenUrl}`, }, - previewUrls: ['https://www.rocket.chat/'], + previewUrls: [chosenUrl], }) .expect('Content-Type', 'application/json') .expect(200) @@ -1410,34 +1458,40 @@ describe('[Chat]', () => { msgId = res.body.message._id; }); - // process is async now so wait for a sec - await sleep(1000); - - await request - .get(api('chat.getMessage')) - .set(credentials) - .query({ - msgId, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('message').to.have.property('urls').to.be.an('array').that.has.lengthOf(2); + await retry( + 'Oembed is generated async thats why the retry is required', + async () => { + await request + .get(api('chat.getMessage')) + .set(credentials) + .query({ + msgId, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('message').to.have.property('urls').to.be.an('array').that.has.lengthOf(2); - expect(res.body.message.urls[0]).to.have.property('meta').that.is.an('object').that.is.empty; - expect(res.body.message.urls[1]).to.have.property('meta').that.is.an('object').that.is.not.empty; - }); + expect(res.body.message.urls[0]).to.have.property('meta').that.is.an('object').that.is.empty; + expect(res.body.message.urls[1]).to.have.property('meta').to.have.property('pageTitle', 'Chosen Preview Page'); + }); + }, + { + delayMs: 100, + retries: 5, + }, + ); }); it('should not generate previews if the message contains more than five external URL', async () => { let msgId; const urls = [ - 'https://www.youtube.com/watch?v=no050HN4ojo', - 'https://www.youtube.com/watch?v=9iaSd13mqXA', - 'https://www.youtube.com/watch?v=MW_qsbgt1KQ', - 'https://www.youtube.com/watch?v=hLF1XwH5rd4', - 'https://www.youtube.com/watch?v=Eo-F9hRBbTk', - 'https://www.youtube.com/watch?v=08ms3W7adFI', + mockVideoUrl('over-limit-1'), + mockVideoUrl('over-limit-2'), + mockVideoUrl('over-limit-3'), + mockVideoUrl('over-limit-4'), + mockVideoUrl('over-limit-5'), + mockVideoUrl('over-limit-6'), ]; await request .post(api('chat.sendMessage')) @@ -1474,6 +1528,131 @@ describe('[Chat]', () => { }); }); }); + + it('should embed the metadata of a rich oembed provider response', async () => { + await mockServerSet(mockOembedEndpoint.method, mockOembedEndpoint.path, spotifyOembedResponse); + const msg = mockVideoUrl('rich-provider'); + + let msgId: IMessage['_id']; + await request + .post(api('chat.sendMessage')) + .set(credentials) + .send({ message: { rid: testChannel._id, msg } }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + msgId = res.body.message._id; + }); + + await retry( + 'Oembed is generated async thats why the retry is required', + async () => { + await request + .get(api('chat.getMessage')) + .set(credentials) + .query({ + msgId, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('message').to.have.property('urls').to.be.an('array').that.has.lengthOf(1); + + const { meta } = res.body.message.urls[0]; + expect(meta).to.have.property('oembedUrl', msg); + expect(meta).to.have.property('oembedTitle', 'Never Gonna Give You Up'); + expect(meta).to.have.property('oembedType', 'rich'); + expect(meta).to.have.property('oembedProviderName', 'Spotify'); + expect(meta).to.have.property('oembedThumbnailUrl', spotifyOembedResponse.thumbnail_url); + expect(meta).to.have.property('oembedHtml').to.have.string('