Files
plspace/src/lib/api/endpoints.test.ts
T
2026-07-30 19:45:40 +09:00

261 lines
8.4 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest'
import { ApiClient } from './client'
import {
fetchNotifications,
fetchQuotes,
postStatus,
setEmojiReaction,
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('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' }])
})
})