Normalize Relationship with zod

This commit is contained in:
Alex Gleason 2023-05-02 18:49:13 -05:00
parent 489145ffb8
commit 0016aeacec
No known key found for this signature in database
GPG key ID: 7211D1F99744FBB7
11 changed files with 48 additions and 212 deletions

View file

@ -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);
});

View file

@ -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);
});

View file

@ -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('<SubscribeButton />', () => {
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(<SubscribeButton account={account} />, 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(<SubscribeButton account={account} />, 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(<SubscribeButton account={account} />, 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(<SubscribeButton account={account} />, 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(<SubscribeButton account={account} />, 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(<SubscribeButton account={account} />, 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(<SubscribeButton account={account} />, 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(<SubscribeButton account={account} />, null, store);
// expect(screen.getByTestId('icon-button').title).toEqual(`Subscribe to notifications from @${account.acct}`);
// });
// });
// });
// });
});

View file

@ -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> = {}): Ad {
}, props));
}
export { buildCard, buildGroup, buildGroupRelationship, buildGroupTag, buildAd };
function buildRelationship(props: Partial<Relationship> = {}): Relationship {
return relationshipSchema.parse(Object.assign({
id: uuidv4(),
}, props));
}
export {
buildCard,
buildGroup,
buildGroupRelationship,
buildGroupTag,
buildAd,
buildRelationship,
};

View file

@ -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';

View file

@ -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<string, any>) => {
return RelationshipRecord(
ImmutableMap(fromJS(relationship)),
);
};

View file

@ -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 })]);
});
});

View file

@ -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 })));
});
});

View file

@ -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<typeof normalizeRelationship>;
type State = ImmutableMap<string, Relationship>;
type APIEntities = Array<APIEntity>;
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<string, Relationship>(), action: AnyAction) {

View file

@ -19,4 +19,4 @@ const relationshipSchema = z.object({
type Relationship = z.infer<typeof relationshipSchema>;
export { relationshipSchema, Relationship };
export { relationshipSchema, type Relationship };

View file

@ -21,7 +21,6 @@ import {
NotificationRecord,
PollRecord,
PollOptionRecord,
RelationshipRecord,
StatusEditRecord,
StatusRecord,
TagRecord,
@ -52,7 +51,6 @@ type Mention = ReturnType<typeof MentionRecord>;
type Notification = ReturnType<typeof NotificationRecord>;
type Poll = ReturnType<typeof PollRecord>;
type PollOption = ReturnType<typeof PollOptionRecord>;
type Relationship = ReturnType<typeof RelationshipRecord>;
type StatusEdit = ReturnType<typeof StatusEditRecord>;
type Tag = ReturnType<typeof TagRecord>;
@ -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';