From 0016aeacec1c99db39b6466689f758c9fe55f250 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 2 May 2023 18:49:13 -0500 Subject: [PATCH] Normalize Relationship with zod --- .../actions/__tests__/account-notes.test.ts | 5 +- .../actions/__tests__/accounts.test.ts | 5 +- .../__tests__/subscribe-button.test.tsx | 161 +----------------- app/soapbox/jest/factory.ts | 17 +- app/soapbox/normalizers/index.ts | 1 - app/soapbox/normalizers/relationship.ts | 35 ---- app/soapbox/queries/__tests__/chats.test.ts | 7 +- .../queries/__tests__/relationships.test.ts | 6 +- app/soapbox/reducers/relationships.ts | 17 +- app/soapbox/schemas/relationship.ts | 2 +- app/soapbox/types/entities.ts | 4 +- 11 files changed, 48 insertions(+), 212 deletions(-) delete mode 100644 app/soapbox/normalizers/relationship.ts diff --git a/app/soapbox/actions/__tests__/account-notes.test.ts b/app/soapbox/actions/__tests__/account-notes.test.ts index 8b85eecc5..a00a9d877 100644 --- a/app/soapbox/actions/__tests__/account-notes.test.ts +++ b/app/soapbox/actions/__tests__/account-notes.test.ts @@ -1,10 +1,11 @@ import { Map as ImmutableMap } from 'immutable'; import { __stub } from 'soapbox/api'; +import { buildRelationship } from 'soapbox/jest/factory'; import { mockStore, rootState } from 'soapbox/jest/test-helpers'; import { ReducerRecord, EditRecord } from 'soapbox/reducers/account-notes'; -import { normalizeAccount, normalizeRelationship } from '../../normalizers'; +import { normalizeAccount } from '../../normalizers'; import { changeAccountNoteComment, initAccountNoteModal, submitAccountNote } from '../account-notes'; import type { Account } from 'soapbox/types/entities'; @@ -66,7 +67,7 @@ describe('initAccountNoteModal()', () => { beforeEach(() => { const state = rootState - .set('relationships', ImmutableMap({ '1': normalizeRelationship({ note: 'hello' }) })); + .set('relationships', ImmutableMap({ '1': buildRelationship({ note: 'hello' }) })); store = mockStore(state); }); diff --git a/app/soapbox/actions/__tests__/accounts.test.ts b/app/soapbox/actions/__tests__/accounts.test.ts index d9faa0213..c13f8ef90 100644 --- a/app/soapbox/actions/__tests__/accounts.test.ts +++ b/app/soapbox/actions/__tests__/accounts.test.ts @@ -1,10 +1,11 @@ import { Map as ImmutableMap } from 'immutable'; import { __stub } from 'soapbox/api'; +import { buildRelationship } from 'soapbox/jest/factory'; import { mockStore, rootState } from 'soapbox/jest/test-helpers'; import { ListRecord, ReducerRecord } from 'soapbox/reducers/user-lists'; -import { normalizeAccount, normalizeInstance, normalizeRelationship } from '../../normalizers'; +import { normalizeAccount, normalizeInstance } from '../../normalizers'; import { authorizeFollowRequest, blockAccount, @@ -1340,7 +1341,7 @@ describe('fetchRelationships()', () => { describe('without newAccountIds', () => { beforeEach(() => { const state = rootState - .set('relationships', ImmutableMap({ [id]: normalizeRelationship({}) })) + .set('relationships', ImmutableMap({ [id]: buildRelationship() })) .set('me', '123'); store = mockStore(state); }); diff --git a/app/soapbox/features/ui/components/__tests__/subscribe-button.test.tsx b/app/soapbox/features/ui/components/__tests__/subscribe-button.test.tsx index d0ec92f96..5edc9636b 100644 --- a/app/soapbox/features/ui/components/__tests__/subscribe-button.test.tsx +++ b/app/soapbox/features/ui/components/__tests__/subscribe-button.test.tsx @@ -1,8 +1,9 @@ -// import { Map as ImmutableMap } from 'immutable'; import React from 'react'; -import { render, screen } from '../../../../jest/test-helpers'; -import { normalizeAccount, normalizeRelationship } from '../../../../normalizers'; +import { buildRelationship } from 'soapbox/jest/factory'; +import { render, screen } from 'soapbox/jest/test-helpers'; +import { normalizeAccount } from 'soapbox/normalizers'; + import SubscribeButton from '../subscription-button'; import type { ReducerAccount } from 'soapbox/reducers/accounts'; @@ -19,162 +20,10 @@ describe('', () => { describe('with "accountNotifies" disabled', () => { it('renders nothing', () => { - const account = normalizeAccount({ ...justin, relationship: normalizeRelationship({ following: true }) }) as ReducerAccount; + const account = normalizeAccount({ ...justin, relationship: buildRelationship({ following: true }) }) as ReducerAccount; render(, undefined, store); expect(screen.queryAllByTestId('icon-button')).toHaveLength(0); }); }); - - // describe('with "accountNotifies" enabled', () => { - // beforeEach(() => { - // store = { - // ...store, - // instance: normalizeInstance({ - // version: '3.4.1 (compatible; TruthSocial 1.0.0)', - // software: 'TRUTHSOCIAL', - // pleroma: ImmutableMap({}), - // }), - // }; - // }); - - // describe('when the relationship is requested', () => { - // beforeEach(() => { - // account = normalizeAccount({ ...account, relationship: normalizeRelationship({ requested: true }) }); - - // store = { - // ...store, - // accounts: ImmutableMap({ - // '1': account, - // }), - // }; - // }); - - // it('renders the button', () => { - // render(, null, store); - // expect(screen.getByTestId('icon-button')).toBeInTheDocument(); - // }); - - // describe('when the user "isSubscribed"', () => { - // beforeEach(() => { - // account = normalizeAccount({ - // ...account, - // relationship: normalizeRelationship({ requested: true, notifying: true }), - // }); - - // store = { - // ...store, - // accounts: ImmutableMap({ - // '1': account, - // }), - // }; - // }); - - // it('renders the unsubscribe button', () => { - // render(, null, store); - // expect(screen.getByTestId('icon-button').title).toEqual(`Unsubscribe to notifications from @${account.acct}`); - // }); - // }); - - // describe('when the user is not "isSubscribed"', () => { - // beforeEach(() => { - // account = normalizeAccount({ - // ...account, - // relationship: normalizeRelationship({ requested: true, notifying: false }), - // }); - - // store = { - // ...store, - // accounts: ImmutableMap({ - // '1': account, - // }), - // }; - // }); - - // it('renders the unsubscribe button', () => { - // render(, null, store); - // expect(screen.getByTestId('icon-button').title).toEqual(`Subscribe to notifications from @${account.acct}`); - // }); - // }); - // }); - - // describe('when the user is not following the account', () => { - // beforeEach(() => { - // account = normalizeAccount({ ...account, relationship: normalizeRelationship({ following: false }) }); - - // store = { - // ...store, - // accounts: ImmutableMap({ - // '1': account, - // }), - // }; - // }); - - // it('renders nothing', () => { - // render(, null, store); - // expect(screen.queryAllByTestId('icon-button')).toHaveLength(0); - // }); - // }); - - // describe('when the user is following the account', () => { - // beforeEach(() => { - // account = normalizeAccount({ ...account, relationship: normalizeRelationship({ following: true }) }); - - // store = { - // ...store, - // accounts: ImmutableMap({ - // '1': account, - // }), - // }; - // }); - - // it('renders the button', () => { - // render(, null, store); - // expect(screen.getByTestId('icon-button')).toBeInTheDocument(); - // }); - - // describe('when the user "isSubscribed"', () => { - // beforeEach(() => { - // account = normalizeAccount({ - // ...account, - // relationship: normalizeRelationship({ requested: true, notifying: true }), - // }); - - // store = { - // ...store, - // accounts: ImmutableMap({ - // '1': account, - // }), - // }; - // }); - - // it('renders the unsubscribe button', () => { - // render(, null, store); - // expect(screen.getByTestId('icon-button').title).toEqual(`Unsubscribe to notifications from @${account.acct}`); - // }); - // }); - - // describe('when the user is not "isSubscribed"', () => { - // beforeEach(() => { - // account = normalizeAccount({ - // ...account, - // relationship: normalizeRelationship({ requested: true, notifying: false }), - // }); - - // store = { - // ...store, - // accounts: ImmutableMap({ - // '1': account, - // }), - // }; - // }); - - // it('renders the unsubscribe button', () => { - // render(, null, store); - // expect(screen.getByTestId('icon-button').title).toEqual(`Subscribe to notifications from @${account.acct}`); - // }); - // }); - // }); - // }); - }); diff --git a/app/soapbox/jest/factory.ts b/app/soapbox/jest/factory.ts index ca9f2b8f0..07f4bc7d2 100644 --- a/app/soapbox/jest/factory.ts +++ b/app/soapbox/jest/factory.ts @@ -6,11 +6,13 @@ import { groupSchema, groupRelationshipSchema, groupTagSchema, + relationshipSchema, type Ad, type Card, type Group, type GroupRelationship, type GroupTag, + type Relationship, } from 'soapbox/schemas'; // TODO: there's probably a better way to create these factory functions. @@ -46,4 +48,17 @@ function buildAd(props: Partial = {}): Ad { }, props)); } -export { buildCard, buildGroup, buildGroupRelationship, buildGroupTag, buildAd }; \ No newline at end of file +function buildRelationship(props: Partial = {}): Relationship { + return relationshipSchema.parse(Object.assign({ + id: uuidv4(), + }, props)); +} + +export { + buildCard, + buildGroup, + buildGroupRelationship, + buildGroupTag, + buildAd, + buildRelationship, +}; \ No newline at end of file diff --git a/app/soapbox/normalizers/index.ts b/app/soapbox/normalizers/index.ts index ef7dbd7ca..d22bce0c9 100644 --- a/app/soapbox/normalizers/index.ts +++ b/app/soapbox/normalizers/index.ts @@ -20,7 +20,6 @@ export { LocationRecord, normalizeLocation } from './location'; export { MentionRecord, normalizeMention } from './mention'; export { NotificationRecord, normalizeNotification } from './notification'; export { PollRecord, PollOptionRecord, normalizePoll } from './poll'; -export { RelationshipRecord, normalizeRelationship } from './relationship'; export { StatusRecord, normalizeStatus } from './status'; export { StatusEditRecord, normalizeStatusEdit } from './status-edit'; export { TagRecord, normalizeTag } from './tag'; diff --git a/app/soapbox/normalizers/relationship.ts b/app/soapbox/normalizers/relationship.ts deleted file mode 100644 index f492a00e9..000000000 --- a/app/soapbox/normalizers/relationship.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Relationship normalizer: - * Converts API relationships into our internal format. - * @see {@link https://docs.joinmastodon.org/entities/relationship/} - */ -import { - Map as ImmutableMap, - Record as ImmutableRecord, - fromJS, -} from 'immutable'; - -// https://docs.joinmastodon.org/entities/relationship/ -// https://api.pleroma.social/#operation/AccountController.relationships -export const RelationshipRecord = ImmutableRecord({ - blocked_by: false, - blocking: false, - domain_blocking: false, - endorsed: false, - followed_by: false, - following: false, - id: '', - muting: false, - muting_notifications: false, - note: '', - notifying: false, - requested: false, - showing_reblogs: false, - subscribing: false, -}); - -export const normalizeRelationship = (relationship: Record) => { - return RelationshipRecord( - ImmutableMap(fromJS(relationship)), - ); -}; diff --git a/app/soapbox/queries/__tests__/chats.test.ts b/app/soapbox/queries/__tests__/chats.test.ts index 65bb6294d..981250456 100644 --- a/app/soapbox/queries/__tests__/chats.test.ts +++ b/app/soapbox/queries/__tests__/chats.test.ts @@ -3,8 +3,9 @@ import sumBy from 'lodash/sumBy'; import { useEffect } from 'react'; import { __stub } from 'soapbox/api'; +import { buildRelationship } from 'soapbox/jest/factory'; import { createTestStore, mockStore, queryClient, renderHook, rootState, waitFor } from 'soapbox/jest/test-helpers'; -import { normalizeChatMessage, normalizeRelationship } from 'soapbox/normalizers'; +import { normalizeChatMessage } from 'soapbox/normalizers'; import { normalizeEmojiReaction } from 'soapbox/normalizers/emoji-reaction'; import { Store } from 'soapbox/store'; import { ChatMessage } from 'soapbox/types/entities'; @@ -120,7 +121,7 @@ describe('useChatMessages', () => { const state = rootState .set( 'relationships', - ImmutableMap({ '1': normalizeRelationship({ blocked_by: true }) }), + ImmutableMap({ '1': buildRelationship({ blocked_by: true }) }), ); store = mockStore(state); }); @@ -239,7 +240,7 @@ describe('useChat()', () => { mock.onGet(`/api/v1/pleroma/chats/${chat.id}`).reply(200, chat); mock .onGet(`/api/v1/accounts/relationships?id[]=${chat.account.id}`) - .reply(200, [normalizeRelationship({ id: relationshipId, blocked_by: true })]); + .reply(200, [buildRelationship({ id: relationshipId, blocked_by: true })]); }); }); diff --git a/app/soapbox/queries/__tests__/relationships.test.ts b/app/soapbox/queries/__tests__/relationships.test.ts index 6466da7ff..02db36166 100644 --- a/app/soapbox/queries/__tests__/relationships.test.ts +++ b/app/soapbox/queries/__tests__/relationships.test.ts @@ -1,8 +1,8 @@ import { useEffect } from 'react'; import { __stub } from 'soapbox/api'; +import { buildRelationship } from 'soapbox/jest/factory'; import { createTestStore, queryClient, renderHook, rootState, waitFor } from 'soapbox/jest/test-helpers'; -import { normalizeRelationship } from 'soapbox/normalizers'; import { Store } from 'soapbox/store'; import { useFetchRelationships } from '../relationships'; @@ -25,7 +25,7 @@ describe('useFetchRelationships()', () => { __stub((mock) => { mock .onGet(`/api/v1/accounts/relationships?id[]=${id}`) - .reply(200, [normalizeRelationship({ id, blocked_by: true })]); + .reply(200, [buildRelationship({ id, blocked_by: true })]); }); }); @@ -55,7 +55,7 @@ describe('useFetchRelationships()', () => { __stub((mock) => { mock .onGet(`/api/v1/accounts/relationships?id[]=${ids[0]}&id[]=${ids[1]}`) - .reply(200, ids.map((id) => normalizeRelationship({ id, blocked_by: true }))); + .reply(200, ids.map((id) => buildRelationship({ id, blocked_by: true }))); }); }); diff --git a/app/soapbox/reducers/relationships.ts b/app/soapbox/reducers/relationships.ts index 2eb035ec4..40d062f78 100644 --- a/app/soapbox/reducers/relationships.ts +++ b/app/soapbox/reducers/relationships.ts @@ -2,7 +2,7 @@ import { List as ImmutableList, Map as ImmutableMap } from 'immutable'; import get from 'lodash/get'; import { STREAMING_FOLLOW_RELATIONSHIPS_UPDATE } from 'soapbox/actions/streaming'; -import { normalizeRelationship } from 'soapbox/normalizers/relationship'; +import { type Relationship, relationshipSchema } from 'soapbox/schemas'; import { ACCOUNT_NOTE_SUBMIT_SUCCESS } from '../actions/account-notes'; import { @@ -35,13 +35,16 @@ import { import type { AnyAction } from 'redux'; import type { APIEntity } from 'soapbox/types/entities'; -type Relationship = ReturnType; type State = ImmutableMap; type APIEntities = Array; const normalizeRelationships = (state: State, relationships: APIEntities) => { relationships.forEach(relationship => { - state = state.set(relationship.id, normalizeRelationship(relationship)); + try { + state = state.set(relationship.id, relationshipSchema.parse(relationship)); + } catch (_e) { + // do nothing + } }); return state; @@ -84,8 +87,12 @@ const followStateToRelationship = (followState: string) => { }; const updateFollowRelationship = (state: State, id: string, followState: string) => { - const map = followStateToRelationship(followState); - return state.update(id, normalizeRelationship({}), relationship => relationship.merge(map)); + const relationship = state.get(id) || relationshipSchema.parse({ id }); + + return state.set(id, { + ...relationship, + ...followStateToRelationship(followState), + }); }; export default function relationships(state: State = ImmutableMap(), action: AnyAction) { diff --git a/app/soapbox/schemas/relationship.ts b/app/soapbox/schemas/relationship.ts index 7d1e109c8..003cf747a 100644 --- a/app/soapbox/schemas/relationship.ts +++ b/app/soapbox/schemas/relationship.ts @@ -19,4 +19,4 @@ const relationshipSchema = z.object({ type Relationship = z.infer; -export { relationshipSchema, Relationship }; \ No newline at end of file +export { relationshipSchema, type Relationship }; \ No newline at end of file diff --git a/app/soapbox/types/entities.ts b/app/soapbox/types/entities.ts index ebbdd197d..27082f76f 100644 --- a/app/soapbox/types/entities.ts +++ b/app/soapbox/types/entities.ts @@ -21,7 +21,6 @@ import { NotificationRecord, PollRecord, PollOptionRecord, - RelationshipRecord, StatusEditRecord, StatusRecord, TagRecord, @@ -52,7 +51,6 @@ type Mention = ReturnType; type Notification = ReturnType; type Poll = ReturnType; type PollOption = ReturnType; -type Relationship = ReturnType; type StatusEdit = ReturnType; type Tag = ReturnType; @@ -96,7 +94,6 @@ export { Notification, Poll, PollOption, - Relationship, Status, StatusEdit, Tag, @@ -111,4 +108,5 @@ export type { Group, GroupMember, GroupRelationship, + Relationship, } from 'soapbox/schemas'; \ No newline at end of file