diff --git a/frontend/src/utils/share/buildHotlinkViewerPath.spec.ts b/frontend/src/utils/share/buildHotlinkViewerPath.spec.ts new file mode 100644 index 0000000..110ee63 --- /dev/null +++ b/frontend/src/utils/share/buildHotlinkViewerPath.spec.ts @@ -0,0 +1,8 @@ +import { describe, expect, it } from 'vitest'; +import { buildHotlinkViewerPath } from '@/utils/share/buildHotlinkViewerPath'; + +describe('buildHotlinkViewerPath', () => { + it('places upload id and secret in the viewer path', () => { + expect(buildHotlinkViewerPath('upload-uuid', 'url-safe-secret')).toBe('/h/upload-uuid/url-safe-secret'); + }); +}); diff --git a/frontend/src/utils/share/buildHotlinkViewerPath.ts b/frontend/src/utils/share/buildHotlinkViewerPath.ts new file mode 100644 index 0000000..acb369c --- /dev/null +++ b/frontend/src/utils/share/buildHotlinkViewerPath.ts @@ -0,0 +1 @@ +export const buildHotlinkViewerPath = (uploadId: string, secret: string): string => `/h/${uploadId}/${secret}`; diff --git a/frontend/src/utils/share/buildUploadedShareUrls.spec.ts b/frontend/src/utils/share/buildUploadedShareUrls.spec.ts new file mode 100644 index 0000000..12fd42a --- /dev/null +++ b/frontend/src/utils/share/buildUploadedShareUrls.spec.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from 'vitest'; +import { config } from '@/config'; +import type { UploadQueueEntry } from '@/types/upload/UploadQueueEntry'; +import { buildUploadedShareUrls } from '@/utils/share/buildUploadedShareUrls'; + +const { clearnet, onion } = config.shareOrigins; + +const uploaded = ( + mode: 'zk' | 'hotlink', + overrides: Partial> = {} +): UploadQueueEntry => ({ + status: 'uploaded', + message: 'Uploaded', + uploadId: 'upload-id', + deleteToken: 'delete-token', + urlSecret: 'url-secret', + mode, + ...overrides +}); + +describe('buildUploadedShareUrls', () => { + it('returns null unless the upload succeeded', () => { + const statuses = ['idle', 'queued', 'preparing', 'uploading', 'failed', 'skipped'] as const; + + for (const status of statuses) { + expect(buildUploadedShareUrls({ status, message: status }, 'photo.jpg')).toBeNull(); + } + }); + + it('builds zero-knowledge viewer, delete, and clearnet text embeds', () => { + const urls = buildUploadedShareUrls( + uploaded('zk', { deleteToken: 'a+b/c', urlSecret: 'fragment_key' }), + 'a & "shot"' + ); + + expect(urls).toEqual({ + mode: 'zk', + viewer: { + clearnet: `${clearnet}/v/upload-id#fragment_key`, + onion: `${onion}/v/upload-id#fragment_key` + }, + delete: { + clearnet: `${clearnet}/d/upload-id?token=a%2Bb%2Fc`, + onion: `${onion}/d/upload-id?token=a%2Bb%2Fc` + }, + markdownLink: `[a & "shot"](${clearnet}/v/upload-id#fragment_key)`, + htmlLink: `a <photo> & "shot"` + }); + }); + + it('builds hotlink viewer, direct image, delete, and clearnet image embeds', () => { + const urls = buildUploadedShareUrls( + uploaded('hotlink', { deleteToken: 'a+b/c', urlSecret: 'path_secret' }), + 'a & "shot"' + ); + + const directClearnet = `${clearnet}/i/upload-id/path_secret`; + + expect(urls).toEqual({ + mode: 'hotlink', + viewer: { + clearnet: `${clearnet}/h/upload-id/path_secret`, + onion: `${onion}/h/upload-id/path_secret` + }, + direct: { + clearnet: directClearnet, + onion: `${onion}/i/upload-id/path_secret` + }, + delete: { + clearnet: `${clearnet}/d/upload-id?token=a%2Bb%2Fc`, + onion: `${onion}/d/upload-id?token=a%2Bb%2Fc` + }, + bbcode: `[img]${directClearnet}[/img]`, + markdownImage: `![a & "shot"](${directClearnet})`, + htmlImage: `a <photo> & "shot"` + }); + }); +}); diff --git a/frontend/src/utils/share/buildUploadedShareUrls.ts b/frontend/src/utils/share/buildUploadedShareUrls.ts index c342c84..1112dd8 100644 --- a/frontend/src/utils/share/buildUploadedShareUrls.ts +++ b/frontend/src/utils/share/buildUploadedShareUrls.ts @@ -8,6 +8,7 @@ import { buildMarkdownLink } from '@/utils/share/buildMarkdownLink'; import { config } from '@/config'; import { buildDeletePath } from '@/utils/share/buildDeletePath'; import { buildHotlinkImagePath } from '@/utils/share/buildHotlinkImagePath'; +import { buildHotlinkViewerPath } from '@/utils/share/buildHotlinkViewerPath'; import { buildSiteShareUrl } from '@/utils/share/buildSiteShareUrl'; import { buildZkViewerPath } from '@/utils/share/buildZkViewerPath'; @@ -22,23 +23,21 @@ export const buildUploadedShareUrls = ( } const deletePath = buildDeletePath(upload.uploadId, upload.deleteToken); - const viewerPath = buildZkViewerPath(upload.uploadId, upload.urlSecret); - - const viewer = { - clearnet: buildSiteShareUrl(clearnet, viewerPath), - onion: buildSiteShareUrl(onion, viewerPath) - }; const deleteUrls = { clearnet: buildSiteShareUrl(clearnet, deletePath), onion: buildSiteShareUrl(onion, deletePath) }; - const core = { viewer, delete: deleteUrls }; - if (upload.mode === 'hotlink') { + const viewerPath = buildHotlinkViewerPath(upload.uploadId, upload.urlSecret); const directPath = buildHotlinkImagePath(upload.uploadId, upload.urlSecret); + const viewer = { + clearnet: buildSiteShareUrl(clearnet, viewerPath), + onion: buildSiteShareUrl(onion, viewerPath) + }; + const direct = { clearnet: buildSiteShareUrl(clearnet, directPath), onion: buildSiteShareUrl(onion, directPath) @@ -46,7 +45,8 @@ export const buildUploadedShareUrls = ( return { mode: 'hotlink', - ...core, + delete: deleteUrls, + viewer, direct, bbcode: buildBbcodeImage(direct.clearnet), markdownImage: buildMarkdownImage(direct.clearnet, fileLabel), @@ -54,9 +54,17 @@ export const buildUploadedShareUrls = ( }; } + const viewerPath = buildZkViewerPath(upload.uploadId, upload.urlSecret); + + const viewer = { + clearnet: buildSiteShareUrl(clearnet, viewerPath), + onion: buildSiteShareUrl(onion, viewerPath) + }; + return { mode: 'zk', - ...core, + delete: deleteUrls, + viewer, markdownLink: buildMarkdownLink(fileLabel, viewer.clearnet), htmlLink: buildHtmlLink(fileLabel, viewer.clearnet) };