Skip to content

Commit

Permalink
Address PR review comments: correctly handle edge cases in canDisplay…
Browse files Browse the repository at this point in the history
…LinkPreview (#7796)
  • Loading branch information
denis-tingaikin authored Jan 25, 2025
1 parent 8b10ebc commit faeee2e
Show file tree
Hide file tree
Showing 2 changed files with 163 additions and 10 deletions.
146 changes: 146 additions & 0 deletions packages/presentation/src/___tests___/link-preview.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License")
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//

import { canDisplayLinkPreview, fetchLinkPreviewDetails, type LinkPreviewDetails } from '../link-preview'
import { setMetadata } from '@hcengineering/platform'
import plugin from '../plugin'

const fechFunc =
(responseFunc: (input: RequestInfo | URL, init?: RequestInit) => Response) =>
async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
return responseFunc(input, init)
}

describe('canDisplayLinkPreview', () => {
it('should return false if hostname & title are undefined', () => {
const linkDetails: LinkPreviewDetails = {
image: 'test-image.jpg',
description: 'Test Description'
}
expect(canDisplayLinkPreview(linkDetails)).toBe(false)
})

it('should return false if both image and description are missing', () => {
const linkDetails: LinkPreviewDetails = {
hostname: 'www.example.com',
title: 'Test Title'
}
expect(canDisplayLinkPreview(linkDetails)).toBe(false)
})

it('should return false if both title and description are empty', () => {
const linkDetails: LinkPreviewDetails = {
hostname: 'www.example.com',
image: 'test-image.jpg',
title: '',
description: ''
}
expect(canDisplayLinkPreview(linkDetails)).toBe(false)
})

it('should return true if hostname, image, and title are present', () => {
const linkDetails: LinkPreviewDetails = {
hostname: 'www.example.com',
image: 'test-image.jpg',
title: 'Test Title'
}
expect(canDisplayLinkPreview(linkDetails)).toBe(true)
})

it('should return true if hostname, description, and title are present', () => {
const linkDetails: LinkPreviewDetails = {
hostname: 'www.example.com',
title: 'Test Title',
description: 'Test Description'
}
expect(canDisplayLinkPreview(linkDetails)).toBe(true)
})

it('should return false if hostname, image, and description are present', () => {
const linkDetails: LinkPreviewDetails = {
hostname: 'www.example.com',
image: 'test-image.jpg',
description: 'Test Description'
}
expect(canDisplayLinkPreview(linkDetails)).toBe(false)
})

it('should handle whitespace in title and description', () => {
const linkDetails: LinkPreviewDetails = {
hostname: 'www.example.com',
image: 'test-image.jpg',
title: ' ',
description: ' '
}
expect(canDisplayLinkPreview(linkDetails)).toBe(false)
})

it('should fetch link preview details successfully', async () => {
const fetcher = fechFunc((url) =>
Response.json({
title: 'Test Title',
description: 'Test Description',
url: 'https://www.example.com',
icon: 'https://www.example.com/favicon.ico',
image: 'https://www.example.com/image.jpg'
})
)
const linkDetails = await fetchLinkPreviewDetails('https://www.example.com', 5000, fetcher)
expect(linkDetails).toEqual({
title: 'Test Title',
description: 'Test Description',
url: 'https://www.example.com',
icon: 'https://www.example.com/favicon.ico',
image: 'https://www.example.com/image.jpg'
})
})

it('should handle errors during fetching', async () => {
const fetcher = fechFunc(() => {
throw new Error('something went wrong')
})
expect(await fetchLinkPreviewDetails('https://www.example.com', 5000, fetcher)).toEqual({})
})

it('should handle timeout', async () => {
let fetcherCalled = false
const fetcher = fechFunc((req, params) => {
if (params?.signal === undefined) {
fail('missed signal')
}
fetcherCalled = true
return Response.json({})
})
await fetchLinkPreviewDetails('https://www.example.com', 50, fetcher)
expect(fetcherCalled).toBe(true)
})

it('should add auth token', async () => {
let tokenProvided = false
setMetadata(plugin.metadata.Token, 'token')
const fetcher = fechFunc((req, params) => {
if (!(params?.headers instanceof Headers)) {
fail('missed headers')
}
if (params.headers.get('Authorization') !== 'Bearer token') {
fail('missed token')
}
tokenProvided = true
return Response.json({})
})
await fetchLinkPreviewDetails('https://www.example.com', 50, fetcher)
expect(tokenProvided).toBe(true)
})
})
27 changes: 17 additions & 10 deletions packages/presentation/src/link-preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,27 +30,30 @@ export interface LinkPreviewDetails {
}

export function canDisplayLinkPreview (val: LinkPreviewDetails): boolean {
if (val.hostname === undefined) {
if (isEmpty(val.host) && isEmpty(val.title)) {
return false
}
if (val.image === undefined && val.description?.trim() === '') {
return false
}
if (val.title?.trim() === '' && val.description?.trim() === '') {
if (isEmpty(val.description) && isEmpty(val.image)) {
return false
}

return true
}

export async function fetchLinkPreviewDetails (url: string, timeoutMs = 15000): Promise<LinkPreviewDetails> {
export async function fetchLinkPreviewDetails (
url: string,
timeoutMs = 5000,
fetcher = fetch
): Promise<LinkPreviewDetails> {
try {
const linkPreviewUrl = getMetadata(plugin.metadata.LinkPreviewUrl)
let token: string = ''
const headers = new Headers()
if (getMetadata(plugin.metadata.Token) !== undefined) {
token = getMetadata(plugin.metadata.Token) as string
const token = getMetadata(plugin.metadata.Token) as string
headers.set('Authorization', 'Bearer ' + token)
}
const response = await fetch(`${linkPreviewUrl}/details?q=${url}`, {
headers: { Authorization: 'Bearer ' + token },
const response = await fetcher(`${linkPreviewUrl}/details?q=${url}`, {
headers,
signal: AbortSignal.timeout(timeoutMs)
})
const res = (await response.json()) as LinkPreviewDetails
Expand All @@ -60,3 +63,7 @@ export async function fetchLinkPreviewDetails (url: string, timeoutMs = 15000):
return {}
}
}

function isEmpty (str: string | undefined): boolean {
return str === undefined || str.trim() === ''
}

0 comments on commit faeee2e

Please sign in to comment.