import { afterEach, describe, expect, it, vi } from 'vitest' import { ApiClient } from './client' import { fetchNotifications, fetchQuotes, postStatus, setEmojiReaction, uploadMedia, updateProfileFields, updatePublicProfile, votePoll, } from './endpoints' afterEach(() => { vi.unstubAllGlobals() }) describe('updateProfileFields', () => { it('uses the Pleroma/Mastodon indexed multipart representation', async () => { const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => { expect(init?.method).toBe('PATCH') expect(init?.headers).toBeInstanceOf(Headers) expect((init?.headers as Headers).get('Authorization')).toBe('Bearer token') expect(init?.body).toBeInstanceOf(FormData) const form = init?.body as FormData expect([...form.entries()]).toEqual([ ['fields_attributes[0][name]', 'Website'], ['fields_attributes[0][value]', 'https://example.test'], ['fields_attributes[1][name]', 'plspace:css1'], ['fields_attributes[1][value]', 'body { color: pink; }'], ]) return new Response(JSON.stringify({ id: 'account-1', fields: [] }), { status: 200, headers: { 'Content-Type': 'application/json' }, }) }) vi.stubGlobal('fetch', fetchMock) await updateProfileFields(new ApiClient('example.test', 'token'), [ { name: 'Website', value: 'https://example.test' }, { name: 'plspace:css1', value: 'body { color: pink; }' }, ]) expect(fetchMock).toHaveBeenCalledWith( 'https://example.test/api/v1/accounts/update_credentials', expect.any(Object), ) }) }) describe('updatePublicProfile', () => { it('sends public text, images, fields and Pleroma identity data as multipart form data', async () => { const avatar = new File(['avatar'], 'avatar.png', { type: 'image/png' }) const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => { const form = init?.body as FormData expect(init?.method).toBe('PATCH') expect(form.get('display_name')).toBe('Alice Example') expect(form.get('note')).toBe('My public bio') expect(form.get('avatar')).toBe(avatar) expect(form.get('header')).toBe('') expect(form.get('actor_type')).toBe('Group') expect(form.get('birthday')).toBe('2000-01-02') expect(form.get('show_birthday')).toBe('true') expect(form.get('fields_attributes[0][name]')).toBe('Website') expect(form.get('fields_attributes[0][value]')).toBe('https://example.test') return new Response(JSON.stringify({ id: 'account-1', fields: [] }), { status: 200, headers: { 'Content-Type': 'application/json' }, }) }) vi.stubGlobal('fetch', fetchMock) await updatePublicProfile(new ApiClient('example.test', 'token'), { displayName: 'Alice Example', note: 'My public bio', avatar, header: '', actorType: 'Group', birthday: '2000-01-02', showBirthday: true, fields: [{ name: 'Website', value: 'https://example.test' }], }) }) it('sends an empty sentinel row when all public fields are cleared', async () => { const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => { const form = init?.body as FormData expect(form.get('fields_attributes[0][name]')).toBe('') expect(form.get('fields_attributes[0][value]')).toBe('') return new Response(JSON.stringify({ id: 'account-1', fields: [] }), { status: 200, headers: { 'Content-Type': 'application/json' }, }) }) vi.stubGlobal('fetch', fetchMock) await updatePublicProfile(new ApiClient('example.test', 'token'), { fields: [] }) }) }) describe('media uploads', () => { it('preserves an SWF file and its MIME type in the multipart upload', async () => { const swf = new File(['flash bytes'], 'animation.swf', { type: 'application/x-shockwave-flash', }) const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => { const form = init?.body as FormData expect(init?.method).toBe('POST') expect(form.get('file')).toBe(swf) expect((form.get('file') as File).name).toBe('animation.swf') expect((form.get('file') as File).type).toBe('application/x-shockwave-flash') return new Response( JSON.stringify({ id: 'flash-1', type: 'unknown', url: 'https://media.example.test/animation.swf', preview_url: null, pleroma: { mime_type: 'application/x-shockwave-flash' }, }), { status: 200, headers: { 'Content-Type': 'application/json' } }, ) }) vi.stubGlobal('fetch', fetchMock) await uploadMedia(new ApiClient('example.test', 'token'), swf) expect(fetchMock).toHaveBeenCalledWith( 'https://example.test/api/v1/media', expect.any(Object), ) }) }) describe('poll endpoints', () => { it('submits selected poll option indexes as JSON', async () => { const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => { expect(init?.method).toBe('POST') expect(JSON.parse(String(init?.body))).toEqual({ choices: [0, 2] }) return new Response( JSON.stringify({ id: 'poll-1', expired: false, multiple: true, votes_count: 2, options: [], emojis: [], voted: true, own_votes: [0, 2], }), { status: 200, headers: { 'Content-Type': 'application/json' } }, ) }) vi.stubGlobal('fetch', fetchMock) await votePoll(new ApiClient('example.test', 'token'), 'poll/one', [0, 2]) expect(fetchMock).toHaveBeenCalledWith( 'https://example.test/api/v1/polls/poll%2Fone/votes', expect.any(Object), ) }) it('includes a standard Mastodon/Pleroma poll in a new status', async () => { const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => { expect(JSON.parse(String(init?.body))).toMatchObject({ status: 'Pick a snack', poll: { options: ['Cake', 'Fruit'], expires_in: 3600, multiple: false, }, }) return new Response(JSON.stringify({ id: 'status-1' }), { status: 200, headers: { 'Content-Type': 'application/json' }, }) }) vi.stubGlobal('fetch', fetchMock) await postStatus(new ApiClient('example.test', 'token'), { status: 'Pick a snack', poll: { options: ['Cake', 'Fruit'], expires_in: 3600, multiple: false, }, }) }) }) describe('emoji reaction endpoints', () => { it.each([ [true, 'PUT'], [false, 'DELETE'], ] as const)('uses the Pleroma/Akkoma reaction endpoint (on=%s)', async (on, method) => { const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => { expect(init?.method).toBe(method) return new Response(JSON.stringify({ id: 'status/one' }), { status: 200, headers: { 'Content-Type': 'application/json' }, }) }) vi.stubGlobal('fetch', fetchMock) await setEmojiReaction( new ApiClient('example.test', 'token'), 'status/one', 'party_blob@remote.example', on, ) expect(fetchMock).toHaveBeenCalledWith( 'https://example.test/api/v1/pleroma/statuses/status%2Fone/reactions/party_blob%40remote.example', expect.any(Object), ) }) }) describe('quote endpoints', () => { it('sends both current and legacy quote parameters when composing', async () => { const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => { expect(JSON.parse(String(init?.body))).toMatchObject({ status: 'Commentary', quoted_status_id: 'quoted/one', quote_id: 'quoted/one', }) return new Response(JSON.stringify({ id: 'quote-1' }), { status: 200, headers: { 'Content-Type': 'application/json' }, }) }) vi.stubGlobal('fetch', fetchMock) await postStatus(new ApiClient('example.test', 'token'), { status: 'Commentary', quoted_status_id: 'quoted/one', }) }) it('falls back to the legacy Pleroma quote-list endpoint', async () => { const fetchMock = vi .fn() .mockResolvedValueOnce( new Response(JSON.stringify({ error: 'Not found' }), { status: 404, headers: { 'Content-Type': 'application/json' }, }), ) .mockResolvedValueOnce( new Response(JSON.stringify([{ id: 'quote-1' }]), { status: 200, headers: { 'Content-Type': 'application/json' }, }), ) vi.stubGlobal('fetch', fetchMock) const page = await fetchQuotes(new ApiClient('old-pleroma.example', 'token'), 'status/one', { limit: 20, }) expect(page.items).toEqual([{ id: 'quote-1' }]) expect(fetchMock.mock.calls[0][0]).toBe( 'https://old-pleroma.example/api/v1/statuses/status%2Fone/quotes?limit=20', ) expect(fetchMock.mock.calls[1][0]).toBe( 'https://old-pleroma.example/api/v1/pleroma/statuses/status%2Fone/quotes?limit=20', ) }) }) describe('fetchNotifications', () => { it('defensively applies requested types when a server ignores the filter', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue( new Response( JSON.stringify([ { id: 'favourite-1', type: 'favourite' }, { id: 'mention-1', type: 'mention' }, ]), { status: 200, headers: { 'Content-Type': 'application/json' } }, ), ), ) const page = await fetchNotifications( new ApiClient('egregoros.example', 'token'), { limit: 20 }, ['favourite'], ) expect(page.items).toEqual([{ id: 'favourite-1', type: 'favourite' }]) }) })