From 4a6a76ddd9777be50a782dcbe8a317c98ea1f2b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sat, 28 Oct 2023 20:51:55 +0200 Subject: [PATCH] Use preprocess for instance v1 to v2 conversion, add test for instance schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- src/reducers/instance.ts | 4 +- src/schemas/instance.test.ts | 214 +++++++++++++++++++++++++++++++++++ src/schemas/instance.ts | 191 ++++++++++++++++--------------- 3 files changed, 317 insertions(+), 92 deletions(-) create mode 100644 src/schemas/instance.test.ts diff --git a/src/reducers/instance.ts b/src/reducers/instance.ts index 1f98e097dd..a6524207e8 100644 --- a/src/reducers/instance.ts +++ b/src/reducers/instance.ts @@ -4,7 +4,6 @@ import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; import { ADMIN_CONFIG_UPDATE_REQUEST, ADMIN_CONFIG_UPDATE_SUCCESS } from 'soapbox/actions/admin'; import { PLEROMA_PRELOAD_IMPORT } from 'soapbox/actions/preload'; import { type Instance, instanceSchema } from 'soapbox/schemas'; -import { instanceV1ToV2 } from 'soapbox/schemas/instance'; import KVStore from 'soapbox/storage/kv-store'; import { ConfigDB } from 'soapbox/utils/config-db'; @@ -20,8 +19,7 @@ import type { APIEntity } from 'soapbox/types/entities'; const initialState: Instance = instanceSchema.parse({}); const importInstance = (_state: typeof initialState, instance: APIEntity) => { - if (typeof instance.domain === 'string') return instanceSchema.parse(instance); - return instanceV1ToV2.parse(instance); + return instanceSchema.parse(instance); }; const preloadImport = (state: typeof initialState, action: Record, path: string) => { diff --git a/src/schemas/instance.test.ts b/src/schemas/instance.test.ts new file mode 100644 index 0000000000..a47e1adad7 --- /dev/null +++ b/src/schemas/instance.test.ts @@ -0,0 +1,214 @@ +import { instanceSchema } from './instance'; + +describe('instanceSchema.parse()', () => { + it('normalizes an empty Map', () => { + const expected = { + configuration: { + media_attachments: {}, + chats: { + max_characters: 5000, + max_media_attachments: 1, + }, + groups: { + max_characters_name: 50, + max_characters_description: 160, + }, + polls: { + max_options: 4, + max_characters_per_option: 25, + min_expiration: 300, + max_expiration: 2629746, + }, + statuses: { + max_characters: 500, + max_media_attachments: 4, + }, + translation: { + enabled: false, + }, + urls: {}, + }, + contact: { + email: '', + }, + description: '', + domain: '', + feature_quote: false, + fedibird_capabilities: [], + languages: [], + pleroma: { + metadata: { + account_activation_required: false, + birthday_min_age: 0, + birthday_required: false, + description_limit: 1500, + features: [], + federation: { + enabled: true, + }, + }, + stats: {}, + }, + registrations: { + approval_required: false, + enabled: false, + }, + rules: [], + stats: {}, + title: '', + thumbnail: { + url: '', + }, + usage: { + users: { + active_month: 0, + }, + }, + version: '0.0.0', + }; + + const result = instanceSchema.parse({}); + expect(result).toMatchObject(expected); + }); + + it('normalizes Pleroma instance with Mastodon configuration format', () => { + const instance = require('soapbox/__fixtures__/pleroma-instance.json'); + + const expected = { + configuration: { + statuses: { + max_characters: 5000, + max_media_attachments: Infinity, + }, + polls: { + max_options: 20, + max_characters_per_option: 200, + min_expiration: 0, + max_expiration: 31536000, + }, + }, + }; + + const result = instanceSchema.parse(instance); + expect(result).toMatchObject(expected); + }); + + it('normalizes Mastodon instance with retained configuration', () => { + const instance = require('soapbox/__fixtures__/mastodon-instance.json'); + + const expected = { + configuration: { + statuses: { + max_characters: 500, + max_media_attachments: 4, + characters_reserved_per_url: 23, + }, + media_attachments: { + image_size_limit: 10485760, + image_matrix_limit: 16777216, + video_size_limit: 41943040, + video_frame_rate_limit: 60, + video_matrix_limit: 2304000, + }, + polls: { + max_options: 4, + max_characters_per_option: 50, + min_expiration: 300, + max_expiration: 2629746, + }, + }, + }; + + const result = instanceSchema.parse(instance); + expect(result).toMatchObject(expected); + }); + + it('normalizes Mastodon 3.0.0 instance with default configuration', () => { + const instance = require('soapbox/__fixtures__/mastodon-3.0.0-instance.json'); + + const expected = { + configuration: { + statuses: { + max_characters: 500, + max_media_attachments: 4, + }, + polls: { + max_options: 4, + max_characters_per_option: 25, + min_expiration: 300, + max_expiration: 2629746, + }, + }, + }; + + const result = instanceSchema.parse(instance); + expect(result).toMatchObject(expected); + }); + + it('normalizes Fedibird instance', () => { + const instance = require('soapbox/__fixtures__/fedibird-instance.json'); + const result = instanceSchema.parse(instance); + + // Sets description_limit + expect(result.pleroma.metadata.description_limit).toEqual(1500); + + // Preserves fedibird_capabilities + expect(result.fedibird_capabilities).toEqual(instance.fedibird_capabilities); + }); + + it('normalizes Mitra instance', () => { + const instance = require('soapbox/__fixtures__/mitra-instance.json'); + const result = instanceSchema.parse(instance); + + // Adds configuration and description_limit + expect(result.configuration).toBeTruthy(); + expect(result.pleroma.metadata.description_limit).toBe(1500); + }); + + it('normalizes GoToSocial instance', () => { + const instance = require('soapbox/__fixtures__/gotosocial-instance.json'); + const result = instanceSchema.parse(instance); + + // Normalizes max_toot_chars + expect(result.configuration.statuses.max_characters).toEqual(5000); + expect('max_toot_chars' in result).toBe(false); + + // Adds configuration and description_limit + expect(result.configuration).toBeTruthy(); + expect(result.pleroma.metadata.description_limit).toBe(1500); + }); + + it('normalizes Friendica instance', () => { + const instance = require('soapbox/__fixtures__/friendica-instance.json'); + const result = instanceSchema.parse(instance); + + // Normalizes max_toot_chars + expect(result.configuration.statuses.max_characters).toEqual(200000); + expect('max_toot_chars' in result).toBe(false); + + // Adds configuration and description_limit + expect(result.configuration).toBeTruthy(); + expect(result.pleroma.metadata.description_limit).toBe(1500); + }); + + it('normalizes a Mastodon RC version', () => { + const instance = require('soapbox/__fixtures__/mastodon-instance-rc.json'); + const result = instanceSchema.parse(instance); + + expect(result.version).toEqual('3.5.0-rc1'); + }); + + it('normalizes Pixelfed instance', () => { + const instance = require('soapbox/__fixtures__/pixelfed-instance.json'); + const result = instanceSchema.parse(instance); + expect(result.title).toBe('pixelfed'); + }); + + it('renames Akkoma to Pleroma', () => { + const instance = require('soapbox/__fixtures__/akkoma-instance.json'); + const result = instanceSchema.parse(instance); + + expect(result.version).toEqual('2.7.2 (compatible; Pleroma 2.4.50+akkoma)'); + + }); +}); diff --git a/src/schemas/instance.ts b/src/schemas/instance.ts index 9e51feaeaf..25c640c6ac 100644 --- a/src/schemas/instance.ts +++ b/src/schemas/instance.ts @@ -1,11 +1,15 @@ /* eslint sort-keys: "error" */ import z from 'zod'; +import { PLEROMA, parseVersion } from 'soapbox/utils/features'; + import { accountSchema } from './account'; import { mrfSimpleSchema } from './pleroma'; import { ruleSchema } from './rule'; import { coerceObject, filteredArray, mimeSchema } from './utils'; +const getAttachmentLimit = (software: string | null) => software === PLEROMA ? Infinity : 4; + const fixVersion = (version: string) => { // Handle Mastodon release candidates if (new RegExp(/[0-9.]+rc[0-9]+/g).test(version)) { @@ -50,8 +54,10 @@ const configurationSchema = coerceObject({ min_expiration: z.number().optional().catch(undefined), }), statuses: coerceObject({ + characters_reserved_per_url: z.number().optional().catch(undefined), max_characters: z.number().optional().catch(undefined), max_media_attachments: z.number().optional().catch(undefined), + }), translation: coerceObject({ enabled: z.boolean().catch(false), @@ -131,9 +137,9 @@ const registrations = coerceObject({ }); const statsSchema = coerceObject({ - domain_count: z.number().catch(0), - status_count: z.number().catch(0), - user_count: z.number().catch(0), + domain_count: z.number().optional().catch(undefined), + status_count: z.number().optional().catch(undefined), + user_count: z.number().optional().catch(undefined), }); const thumbnailSchema = coerceObject({ @@ -146,7 +152,96 @@ const usageSchema = coerceObject({ }), }); -const instanceSchema = coerceObject({ +const instanceV1Schema = coerceObject({ + approval_required: z.boolean().catch(false), + configuration: configurationSchema, + contact_account: accountSchema.optional().catch(undefined), + description: z.string().catch(''), + description_limit: z.number().catch(1500), + email: z.string().email().catch(''), + feature_quote: z.boolean().catch(false), + fedibird_capabilities: z.array(z.string()).catch([]), + languages: z.string().array().catch([]), + max_media_attachments: z.number().optional().catch(undefined), + max_toot_chars: z.number().optional().catch(undefined), + nostr: nostrSchema.optional().catch(undefined), + pleroma: pleromaSchema, + poll_limits: pleromaPollLimitsSchema, + registrations: z.boolean().catch(false), + rules: filteredArray(ruleSchema), + short_description: z.string().catch(''), + stats: statsSchema, + thumbnail: z.string().catch(''), + title: z.string().catch(''), + urls: coerceObject({ + streaming_api: z.string().url().optional().catch(undefined), + }), + usage: usageSchema, + version: z.string().catch('0.0.0'), +}); + +const instanceSchema = z.preprocess((data: any) => { + if (data.domain) return data; + + const { + approval_required, + configuration, + contact_account, + description, + description_limit, + email, + max_media_attachments, + max_toot_chars, + poll_limits, + pleroma, + registrations, + short_description, + thumbnail, + urls, + ...instance + } = instanceV1Schema.parse(data); + + const { software } = parseVersion(instance.version); + + return { + ...instance, + configuration: { + ...configuration, + polls: { + ...configuration.polls, + max_characters_per_option: configuration.polls.max_characters_per_option ?? poll_limits.max_option_chars ?? 25, + max_expiration: configuration.polls.max_expiration ?? poll_limits.max_expiration ?? 2629746, + max_options: configuration.polls.max_options ?? poll_limits.max_options ?? 4, + min_expiration: configuration.polls.min_expiration ?? poll_limits.min_expiration ?? 300, + }, + statuses: { + ...configuration.statuses, + max_characters: configuration.statuses.max_characters ?? max_toot_chars ?? 500, + max_media_attachments: configuration.statuses.max_media_attachments ?? max_media_attachments ?? getAttachmentLimit(software), + }, + urls: { + streaming: urls.streaming_api, + }, + }, + contact: { + account: contact_account, + email: email, + }, + description: short_description || description, + pleroma: { + ...pleroma, + metadata: { + ...pleroma.metadata, + description_limit, + }, + }, + registrations: { + approval_required: approval_required, + enabled: registrations, + }, + thumbnail: { url: thumbnail }, + }; +}, coerceObject({ configuration: configurationSchema, contact: contactSchema, description: z.string().catch(''), @@ -162,7 +257,7 @@ const instanceSchema = coerceObject({ thumbnail: thumbnailSchema, title: z.string().catch(''), usage: usageSchema, - version: z.string().catch(''), + version: z.string().catch('0.0.0'), }).transform(({ configuration, ...instance }) => { const version = fixVersion(instance.version); @@ -189,90 +284,8 @@ const instanceSchema = coerceObject({ }, version, }; -}); - -const instanceV1ToV2 = coerceObject({ - approval_required: z.boolean().catch(false), - configuration: configurationSchema, - contact_account: accountSchema.optional().catch(undefined), - description: z.string().catch(''), - description_limit: z.number().catch(1500), - email: z.string().email().catch(''), - feature_quote: z.boolean().catch(false), - fedibird_capabilities: z.array(z.string()).catch([]), - languages: z.string().array().catch([]), - max_media_attachments: z.number().optional().catch(undefined), - max_toot_chars: z.number().optional().catch(undefined), - nostr: nostrSchema.optional().catch(undefined), - pleroma: pleromaSchema, - poll_limits: pleromaPollLimitsSchema, - registrations: z.boolean().catch(false), - rules: filteredArray(ruleSchema), - short_description: z.string().catch(''), - stats: statsSchema, - thumbnail: z.string().catch(''), - title: z.string().catch(''), - urls: coerceObject({ - streaming_api: z.string().url().optional().catch(undefined), - }), - usage: usageSchema, - version: z.string().catch(''), -}).transform(({ - approval_required, - configuration, - contact_account, - description, - description_limit, - email, - max_media_attachments, - max_toot_chars, - poll_limits, - pleroma, - registrations, - short_description, - thumbnail, - urls, - ...instance -}) => instanceSchema.parse({ - ...instance, - configuration: { - ...configuration, - polls: { - ...configuration.polls, - max_characters_per_option: configuration.polls.max_characters_per_option ?? poll_limits.max_option_chars ?? 25, - max_expiration: configuration.polls.max_expiration ?? poll_limits.max_expiration ?? 2629746, - max_options: configuration.polls.max_options ?? poll_limits.max_options ?? 4, - min_expiration: configuration.polls.min_expiration ?? poll_limits.min_expiration ?? 300, - }, - statuses: { - ...configuration.statuses, - max_characters: configuration.statuses.max_characters ?? max_toot_chars ?? 500, - max_media_attachments: configuration.statuses.max_media_attachments ?? max_media_attachments ?? 4, - }, - urls: { - streaming: urls.streaming_api, - }, - }, - contact: { - account: contact_account, - email: email, - }, - description: short_description || description, - pleroma: { - ...pleroma, - metadata: { - ...pleroma.metadata, - description_limit, - }, - }, - registrations: { - approval_required: approval_required, - enabled: registrations, - }, - thumbnail: { url: thumbnail }, -}), -); +})); type Instance = z.infer; -export { instanceSchema, instanceV1ToV2, Instance }; +export { instanceSchema, Instance };