From aee2cde33999537f085427590d53152f21cf4529 Mon Sep 17 00:00:00 2001 From: Sven Thelemann Date: Fri, 1 Aug 2025 16:16:48 +0200 Subject: [PATCH 1/8] feat(friendship)!: redesign table `friendship` In the table `friendship` the columns `a_account_id` and `b_account_id`were renamed to `account_id`and `friend_account_id`, a new column `is_close_friend` was added, the policies were updated. Several friendship related functions were added. Test scripts were updated accordingly. --- src/deploy/function_friendship.sql | 151 +++++++++ src/deploy/table_friendship.sql | 65 ++-- src/revert/function_friendship.sql | 9 + src/revert/table_friendship.sql | 7 +- src/sqitch.plan | 1 + src/verify/function_friendship.sql | 28 ++ src/verify/table_friendship.sql | 5 +- test/fixture/schema_vibetype.definition.sql | 305 ++++++++++++++++-- test/logic/scenario/model/friendship.sql | 98 ++++-- .../utility/model/account_registration.sql | 2 +- test/logic/utility/model/friendship.sql | 145 +++------ 11 files changed, 622 insertions(+), 194 deletions(-) create mode 100644 src/deploy/function_friendship.sql create mode 100644 src/revert/function_friendship.sql create mode 100644 src/verify/function_friendship.sql diff --git a/src/deploy/function_friendship.sql b/src/deploy/function_friendship.sql new file mode 100644 index 00000000..14d7137d --- /dev/null +++ b/src/deploy/function_friendship.sql @@ -0,0 +1,151 @@ +BEGIN; + +-- accept friendship request + +CREATE OR REPLACE FUNCTION vibetype.friendship_accept( + requestor_account_id UUID +) RETURNS VOID AS $$ +DECLARE + _friend_account_id UUID; + _count INTEGER; +BEGIN + + _friend_account_id := vibetype.invoker_account_id(); + + UPDATE vibetype.friendship SET + status = 'accepted'::vibetype.friendship_status + -- updated_by filled by trigger + WHERE account_id = requestor_account_id AND friend_account_id = _friend_account_id + AND status = 'requested'::vibetype.friendship_status; + + GET DIAGNOSTICS _count = ROW_COUNT; + IF _count = 0 THEN + RAISE EXCEPTION 'Friendship request does not exist' USING ERRCODE = 'VTFAC'; + END IF; + + INSERT INTO vibetype.friendship(account_id, friend_account_id, status, created_by) + VALUES (_friend_account_id, requestor_account_id, 'accepted'::vibetype.friendship_status, _friend_account_id); + +END; $$ LANGUAGE plpgsql SECURITY INVOKER; + +COMMENT ON FUNCTION vibetype.friendship_accept(UUID) IS 'Accepts a friendship request.\n\nError codes:\n- **VTFAC** when a corresponding friendship request does not exist.'; + +GRANT EXECUTE ON FUNCTION vibetype.friendship_accept(UUID) TO vibetype_account; + +-- reject or cancel friendship + +CREATE OR REPLACE FUNCTION vibetype.friendship_cancel( + friend_account_id UUID +) RETURNS VOID AS $$ +DECLARE + _account_id UUID; +BEGIN + + _account_id := vibetype.invoker_account_id(); + + DELETE FROM vibetype.friendship f + WHERE (account_id = _account_id AND f.friend_account_id = friendship_cancel.friend_account_id) + OR (account_id = friendship_cancel.friend_account_id AND f.friend_account_id = _account_id); + +END; $$ LANGUAGE plpgsql SECURITY INVOKER; + +COMMENT ON FUNCTION vibetype.friendship_cancel(UUID) IS 'Rejects or cancels a friendship (in both directions).'; + +GRANT EXECUTE ON FUNCTION vibetype.friendship_cancel(UUID) TO vibetype_account; + +-- create notification for a request + +CREATE OR REPLACE FUNCTION vibetype.friendship_notify_request( + friend_account_id UUID, + language TEXT +) RETURNS VOID AS $$ +BEGIN + + INSERT INTO vibetype_private.notification (channel, payload) + VALUES ( + 'friendship_request', + jsonb_pretty(jsonb_build_object( + 'data', jsonb_build_object( + 'requestor_account_id', vibetype.invoker_account_id(), + 'requestee_account_id', friendship_notify_request.friend_account_id + ), + 'template', jsonb_build_object('language', friendship_notify_request.language) + )) + ); + +END; $$ LANGUAGE plpgsql SECURITY DEFINER; + +COMMENT ON FUNCTION vibetype.friendship_notify_request(UUID, TEXT) IS 'Creates a notification for a friendship_request'; + +GRANT EXECUTE ON FUNCTION vibetype.friendship_notify_request(UUID, TEXT) TO vibetype_account; + +-- request friendship + +CREATE OR REPLACE FUNCTION vibetype.friendship_request( + friend_account_id UUID, + language TEXT +) RETURNS VOID AS $$ +DECLARE + _account_id UUID; +BEGIN + + _account_id := vibetype.invoker_account_id(); + + IF EXISTS( + SELECT 1 + FROM vibetype.friendship f + WHERE (f.account_id = _account_id AND f.friend_account_id = friendship_request.friend_account_id) + OR (f.account_id = friendship_request.friend_account_id AND f.friend_account_id = _account_id) + ) + THEN + RAISE EXCEPTION 'Friendship already exists or has already been requested.' USING ERRCODE = 'VTREQ'; + END IF; + + INSERT INTO vibetype.friendship(account_id, friend_account_id, status, created_by) + VALUES (_account_id, friendship_request.friend_account_id, 'requested'::vibetype.friendship_status, _account_id); + + PERFORM vibetype.friendship_notify_request(friendship_request.friend_account_id, friendship_request.language); + +END; $$ LANGUAGE plpgsql SECURITY INVOKER; + +COMMENT ON FUNCTION vibetype.friendship_request(UUID, TEXT) IS 'Starts a new friendship request.\n\nError codes:\n- **VTREQ** when the friendship already exists or has already been requested.'; + +GRANT EXECUTE ON FUNCTION vibetype.friendship_request(UUID, TEXT) TO vibetype_account; + + +-- toggle closeness of friendship + +CREATE OR REPLACE FUNCTION vibetype.friendship_toggle_closeness( + friend_account_id UUID +) RETURNS BOOLEAN AS $$ +DECLARE + _account_id UUID; + _is_close_friend BOOLEAN; + current_status vibetype.friendship_status; +BEGIN + + _account_id := vibetype.invoker_account_id(); + + SELECT status INTO current_status + FROM vibetype.friendship f + WHERE f.account_id = _account_id AND f.friend_account_id = friendship_toggle_closeness.friend_account_id; + + IF current_status IS NULL OR current_status != 'accepted'::vibetype.friendship_status THEN + RAISE EXCEPTION 'Friendship does not exist' USING ERRCODE = 'VTFTC'; + END IF; + + UPDATE vibetype.friendship f + SET is_close_friend = NOT is_close_friend + WHERE account_id = vibetype.invoker_account_id() + AND f.friend_account_id = friendship_toggle_closeness.friend_account_id + RETURNING is_close_friend INTO _is_close_friend; + + RETURN _is_close_friend; + +END; $$ LANGUAGE plpgsql SECURITY INVOKER; + +COMMENT ON FUNCTION vibetype.friendship_toggle_closeness(UUID) IS 'Toggles a frien1dship relation between ''not a close friend'' and ''close friend''.\n\nError codes:\n- **VTFTC** when the friendship does not exist.'; + +GRANT EXECUTE ON FUNCTION vibetype.friendship_toggle_closeness(UUID) TO vibetype_account; + +COMMIT; diff --git a/src/deploy/table_friendship.sql b/src/deploy/table_friendship.sql index 4744ce89..2553f04c 100644 --- a/src/deploy/table_friendship.sql +++ b/src/deploy/table_friendship.sql @@ -1,10 +1,10 @@ -BEGIN; - CREATE TABLE vibetype.friendship ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - a_account_id UUID NOT NULL REFERENCES vibetype.account(id) ON DELETE CASCADE, - b_account_id UUID NOT NULL REFERENCES vibetype.account(id) ON DELETE CASCADE, + account_id UUID NOT NULL REFERENCES vibetype.account(id) ON DELETE CASCADE, + friend_account_id UUID NOT NULL REFERENCES vibetype.account(id) ON DELETE CASCADE, + + is_close_friend BOOLEAN NOT NULL DEFAULT false, status vibetype.friendship_status NOT NULL DEFAULT 'requested'::vibetype.friendship_status, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -12,11 +12,9 @@ CREATE TABLE vibetype.friendship ( updated_at TIMESTAMP WITH TIME ZONE, updated_by UUID REFERENCES vibetype.account(id) ON DELETE SET NULL, - UNIQUE (a_account_id, b_account_id), - CONSTRAINT friendship_creator_participant CHECK (created_by = a_account_id or created_by = b_account_id), - CONSTRAINT friendship_creator_updater_difference CHECK (created_by <> updated_by), - CONSTRAINT friendship_ordering CHECK (a_account_id < b_account_id), - CONSTRAINT friendship_updater_participant CHECK (updated_by IS NULL or updated_by = a_account_id or updated_by = b_account_id) + UNIQUE (account_id, friend_account_id), + CONSTRAINT friendship_creator_friend CHECK (account_id <> friend_account_id), + CONSTRAINT friendship_creator_participant CHECK (created_by = account_id) ); CREATE INDEX idx_friendship_created_by ON vibetype.friendship USING btree (created_by); @@ -24,8 +22,9 @@ CREATE INDEX idx_friendship_updated_by ON vibetype.friendship USING btree (updat COMMENT ON TABLE vibetype.friendship IS 'A friend relation together with its status.'; COMMENT ON COLUMN vibetype.friendship.id IS E'@omit create,update\nThe friend relation''s internal id.'; -COMMENT ON COLUMN vibetype.friendship.a_account_id IS E'@omit update\nThe ''left'' side of the friend relation. It must be lexically less than the ''right'' side.'; -COMMENT ON COLUMN vibetype.friendship.b_account_id IS E'@omit update\nThe ''right'' side of the friend relation. It must be lexically greater than the ''left'' side.'; +COMMENT ON COLUMN vibetype.friendship.account_id IS E'@omit update\nThe one side of the friend relation. If the status is ''requested'' then it is the requestor account.'; +COMMENT ON COLUMN vibetype.friendship.friend_account_id IS E'@omit update\nThe other side of the friend relation. If the status is ''requested'' then it is the requestee account.'; +COMMENT ON COLUMN vibetype.friendship.is_close_friend IS E'@omit create\nThe flag indicating whether account_id considers friend_account_id as a close friend or not.'; COMMENT ON COLUMN vibetype.friendship.status IS E'@omit create\nThe status of the friend relation.'; COMMENT ON COLUMN vibetype.friendship.created_at IS E'@omit create,update\nThe timestamp when the friend relation was created.'; COMMENT ON COLUMN vibetype.friendship.created_by IS E'@omit update\nThe account that created the friend relation was created.'; @@ -45,20 +44,19 @@ GRANT INSERT, SELECT, UPDATE, DELETE ON TABLE vibetype.friendship TO vibetype_ac ALTER TABLE vibetype.friendship ENABLE ROW LEVEL SECURITY; +CREATE POLICY friendship_not_blocked ON vibetype.friendship FOR ALL +USING ( + account_id NOT IN (SELECT id FROM vibetype_private.account_block_ids()) + AND friend_account_id NOT IN (SELECT id FROM vibetype_private.account_block_ids()) +); + -- Only allow interactions with friendships in which the current user is involved. -CREATE POLICY friendship_existing ON vibetype.friendship FOR ALL +CREATE POLICY friendship_select ON vibetype.friendship FOR SELECT USING ( - ( - vibetype.invoker_account_id() = a_account_id - AND b_account_id NOT IN (SELECT id FROM vibetype_private.account_block_ids()) - ) + account_id = vibetype.invoker_account_id() OR - ( - vibetype.invoker_account_id() = b_account_id - AND a_account_id NOT IN (SELECT id FROM vibetype_private.account_block_ids()) - ) -) -WITH CHECK (FALSE); + friend_account_id = vibetype.invoker_account_id() +); -- Only allow creation by the current user. CREATE POLICY friendship_insert ON vibetype.friendship FOR INSERT @@ -66,9 +64,11 @@ WITH CHECK ( created_by = vibetype.invoker_account_id() ); --- Only allow update by the current user and only the state transition requested -> accepted. -CREATE POLICY friendship_update ON vibetype.friendship FOR UPDATE +-- Only allow update by the current user if it is about accepting a friendship request. +CREATE POLICY friendship_update_accept ON vibetype.friendship FOR UPDATE USING ( + friend_account_id = vibetype.invoker_account_id() + AND status = 'requested'::vibetype.friendship_status ) WITH CHECK ( status = 'accepted'::vibetype.friendship_status @@ -76,4 +76,19 @@ USING ( updated_by = vibetype.invoker_account_id() ); -COMMIT; +-- Only allow update by the current user if it is already an accepted relation. +CREATE POLICY friendship_update_toggle_closeness ON vibetype.friendship FOR UPDATE +USING ( + status = 'accepted'::vibetype.friendship_status + AND + account_id = vibetype.invoker_account_id() +) WITH CHECK ( + status = 'accepted'::vibetype.friendship_status + AND + updated_by = vibetype.invoker_account_id() +); + +CREATE POLICY friendship_delete ON vibetype.friendship FOR DELETE +USING ( + TRUE +); diff --git a/src/revert/function_friendship.sql b/src/revert/function_friendship.sql new file mode 100644 index 00000000..db7c86a7 --- /dev/null +++ b/src/revert/function_friendship.sql @@ -0,0 +1,9 @@ +BEGIN; + +DROP FUNCTION vibetype.friendship_accept(UUID); +DROP FUNCTION vibetype.friendship_cancel(UUID); +DROP FUNCTION vibetype.friendship_notify_request(UUID, TEXT); +DROP FUNCTION vibetype.friendship_request(UUID, TEXT); +DROP FUNCTION vibetype.friendship_toggle_closeness(UUID); + +COMMIT; diff --git a/src/revert/table_friendship.sql b/src/revert/table_friendship.sql index 8d3a165d..17413551 100644 --- a/src/revert/table_friendship.sql +++ b/src/revert/table_friendship.sql @@ -1,8 +1,11 @@ BEGIN; -DROP POLICY friendship_update ON vibetype.friendship; +DROP POLICY friendship_not_blocked ON vibetype.friendship; +DROP POLICY friendship_select ON vibetype.friendship; DROP POLICY friendship_insert ON vibetype.friendship; -DROP POLICY friendship_existing ON vibetype.friendship; +DROP POLICY friendship_update_accept ON vibetype.friendship; +DROP POLICY friendship_update_toggle_closeness ON vibetype.friendship; +DROP POLICY friendship_delete ON vibetype.friendship; DROP TRIGGER vibetype_trigger_friendship_update ON vibetype.friendship; diff --git a/src/sqitch.plan b/src/sqitch.plan index fd0dbbf2..18ee931b 100644 --- a/src/sqitch.plan +++ b/src/sqitch.plan @@ -95,3 +95,4 @@ table_preference_event_location [schema_public table_account_public role_account role_zammad 1970-01-01T00:00:00Z Sven Thelemann # Add role zammad. database_zammad [role_zammad] 1970-01-01T00:00:00Z Sven Thelemann # Add the database for zammad. function_account_search [schema_public table_account_public role_account] 1970-01-01T00:00:00Z Svens Thelemann # Add a function for searching accounts based on a substring query. +function_friendship [schema_public table_friendship role_account] 1970-01-01T00:00:00Z Svens Thelemann # Add functions for handling friendships. diff --git a/src/verify/function_friendship.sql b/src/verify/function_friendship.sql new file mode 100644 index 00000000..5a922070 --- /dev/null +++ b/src/verify/function_friendship.sql @@ -0,0 +1,28 @@ +BEGIN; + +DO $$ +BEGIN + + IF NOT (SELECT pg_catalog.has_function_privilege('vibetype_account', 'vibetype.friendship_accept(UUID)', 'EXECUTE')) THEN + RAISE EXCEPTION 'Test failed: vibetype_account does not have EXECUTE privilege for vibetype.friendship_accept(UUID).'; + END IF; + + IF NOT (SELECT pg_catalog.has_function_privilege('vibetype_account', 'vibetype.friendship_cancel(UUID)', 'EXECUTE')) THEN + RAISE EXCEPTION 'Test failed: vibetype_account does not have EXECUTE privilege for vibetype.friendship_cancel(UUID).'; + END IF; + + IF NOT (SELECT pg_catalog.has_function_privilege('vibetype_account', 'vibetype.friendship_notify_request(UUID, TEXT)', 'EXECUTE')) THEN + RAISE EXCEPTION 'Test failed: vibetype_account does not have EXECUTE privilege for vibetype.friendship_notify_request(UUID, TEXT).'; + END IF; + + IF NOT (SELECT pg_catalog.has_function_privilege('vibetype_account', 'vibetype.friendship_request(UUID, TEXT)', 'EXECUTE')) THEN + RAISE EXCEPTION 'Test failed: vibetype_account does not have EXECUTE privilege for vibetype.friendship_request(UUID, TEXT).'; + END IF; + + IF NOT (SELECT pg_catalog.has_function_privilege('vibetype_account', 'vibetype.friendship_toggle_closeness(UUID)', 'EXECUTE')) THEN + RAISE EXCEPTION 'Test failed: vibetype_account does not have EXECUTE privilege for vibetype.friendship_toggle_closeness(UUID).'; + END IF; + +END $$; + +ROLLBACK; diff --git a/src/verify/table_friendship.sql b/src/verify/table_friendship.sql index 66005c24..b57e3a90 100644 --- a/src/verify/table_friendship.sql +++ b/src/verify/table_friendship.sql @@ -2,8 +2,9 @@ BEGIN; SELECT id, - a_account_id, - b_account_id, + account_id, + friend_account_id, + is_close_friend, status, created_at, created_by, diff --git a/test/fixture/schema_vibetype.definition.sql b/test/fixture/schema_vibetype.definition.sql index 90a680d7..7430cf3a 100644 --- a/test/fixture/schema_vibetype.definition.sql +++ b/test/fixture/schema_vibetype.definition.sql @@ -1310,6 +1310,191 @@ ALTER FUNCTION vibetype.events_organized() OWNER TO ci; COMMENT ON FUNCTION vibetype.events_organized() IS 'Add a function that returns all event ids for which the invoker is the creator.'; +-- +-- Name: friendship_accept(uuid); Type: FUNCTION; Schema: vibetype; Owner: ci +-- + +CREATE FUNCTION vibetype.friendship_accept(requestor_account_id uuid) RETURNS void + LANGUAGE plpgsql + AS $$ +DECLARE + _friend_account_id UUID; + _count INTEGER; +BEGIN + + _friend_account_id := vibetype.invoker_account_id(); + + UPDATE vibetype.friendship SET + status = 'accepted'::vibetype.friendship_status + -- updated_by filled by trigger + WHERE account_id = requestor_account_id AND friend_account_id = _friend_account_id + AND status = 'requested'::vibetype.friendship_status; + + GET DIAGNOSTICS _count = ROW_COUNT; + IF _count = 0 THEN + RAISE EXCEPTION 'Friendship request does not exist' USING ERRCODE = 'VTFAC'; + END IF; + + INSERT INTO vibetype.friendship(account_id, friend_account_id, status, created_by) + VALUES (_friend_account_id, requestor_account_id, 'accepted'::vibetype.friendship_status, _friend_account_id); + +END; $$; + + +ALTER FUNCTION vibetype.friendship_accept(requestor_account_id uuid) OWNER TO ci; + +-- +-- Name: FUNCTION friendship_accept(requestor_account_id uuid); Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON FUNCTION vibetype.friendship_accept(requestor_account_id uuid) IS 'Accepts a friendship request.\n\nError codes:\n- **VTFAC** when a corresponding friendship request does not exist.'; + + +-- +-- Name: friendship_cancel(uuid); Type: FUNCTION; Schema: vibetype; Owner: ci +-- + +CREATE FUNCTION vibetype.friendship_cancel(friend_account_id uuid) RETURNS void + LANGUAGE plpgsql + AS $$ +DECLARE + _account_id UUID; +BEGIN + + _account_id := vibetype.invoker_account_id(); + + DELETE FROM vibetype.friendship f + WHERE (account_id = _account_id AND f.friend_account_id = friendship_cancel.friend_account_id) + OR (account_id = friendship_cancel.friend_account_id AND f.friend_account_id = _account_id); + +END; $$; + + +ALTER FUNCTION vibetype.friendship_cancel(friend_account_id uuid) OWNER TO ci; + +-- +-- Name: FUNCTION friendship_cancel(friend_account_id uuid); Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON FUNCTION vibetype.friendship_cancel(friend_account_id uuid) IS 'Rejects or cancels a friendship (in both directions).'; + + +-- +-- Name: friendship_notify_request(uuid, text); Type: FUNCTION; Schema: vibetype; Owner: ci +-- + +CREATE FUNCTION vibetype.friendship_notify_request(friend_account_id uuid, language text) RETURNS void + LANGUAGE plpgsql SECURITY DEFINER + AS $$ +BEGIN + + INSERT INTO vibetype_private.notification (channel, payload) + VALUES ( + 'friendship_request', + jsonb_pretty(jsonb_build_object( + 'data', jsonb_build_object( + 'requestor_account_id', vibetype.invoker_account_id(), + 'requestee_account_id', friendship_notify_request.friend_account_id + ), + 'template', jsonb_build_object('language', friendship_notify_request.language) + )) + ); + +END; $$; + + +ALTER FUNCTION vibetype.friendship_notify_request(friend_account_id uuid, language text) OWNER TO ci; + +-- +-- Name: FUNCTION friendship_notify_request(friend_account_id uuid, language text); Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON FUNCTION vibetype.friendship_notify_request(friend_account_id uuid, language text) IS 'Creates a notification for a friendship_request'; + + +-- +-- Name: friendship_request(uuid, text); Type: FUNCTION; Schema: vibetype; Owner: ci +-- + +CREATE FUNCTION vibetype.friendship_request(friend_account_id uuid, language text) RETURNS void + LANGUAGE plpgsql + AS $$ +DECLARE + _account_id UUID; +BEGIN + + _account_id := vibetype.invoker_account_id(); + + IF EXISTS( + SELECT 1 + FROM vibetype.friendship f + WHERE (f.account_id = _account_id AND f.friend_account_id = friendship_request.friend_account_id) + OR (f.account_id = friendship_request.friend_account_id AND f.friend_account_id = _account_id) + ) + THEN + RAISE EXCEPTION 'Friendship already exists or has already been requested.' USING ERRCODE = 'VTREQ'; + END IF; + + INSERT INTO vibetype.friendship(account_id, friend_account_id, status, created_by) + VALUES (_account_id, friendship_request.friend_account_id, 'requested'::vibetype.friendship_status, _account_id); + + PERFORM vibetype.friendship_notify_request(friendship_request.friend_account_id, friendship_request.language); + +END; $$; + + +ALTER FUNCTION vibetype.friendship_request(friend_account_id uuid, language text) OWNER TO ci; + +-- +-- Name: FUNCTION friendship_request(friend_account_id uuid, language text); Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON FUNCTION vibetype.friendship_request(friend_account_id uuid, language text) IS 'Starts a new friendship request.\n\nError codes:\n- **VTREQ** when the friendship already exists or has already been requested.'; + + +-- +-- Name: friendship_toggle_closeness(uuid); Type: FUNCTION; Schema: vibetype; Owner: ci +-- + +CREATE FUNCTION vibetype.friendship_toggle_closeness(friend_account_id uuid) RETURNS boolean + LANGUAGE plpgsql + AS $$ +DECLARE + _account_id UUID; + _is_close_friend BOOLEAN; + current_status vibetype.friendship_status; +BEGIN + + _account_id := vibetype.invoker_account_id(); + + SELECT status INTO current_status + FROM vibetype.friendship f + WHERE f.account_id = _account_id AND f.friend_account_id = friendship_toggle_closeness.friend_account_id; + + IF current_status IS NULL OR current_status != 'accepted'::vibetype.friendship_status THEN + RAISE EXCEPTION 'Friendship does not exist' USING ERRCODE = 'VTFTC'; + END IF; + + UPDATE vibetype.friendship f + SET is_close_friend = NOT is_close_friend + WHERE account_id = vibetype.invoker_account_id() + AND f.friend_account_id = friendship_toggle_closeness.friend_account_id + RETURNING is_close_friend INTO _is_close_friend; + + RETURN _is_close_friend; + +END; $$; + + +ALTER FUNCTION vibetype.friendship_toggle_closeness(friend_account_id uuid) OWNER TO ci; + +-- +-- Name: FUNCTION friendship_toggle_closeness(friend_account_id uuid); Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON FUNCTION vibetype.friendship_toggle_closeness(friend_account_id uuid) IS 'Toggles a frien1dship relation between ''not a close friend'' and ''close friend''.\n\nError codes:\n- **VTFTC** when the friendship does not exist.'; + + -- -- Name: guest_claim_array(); Type: FUNCTION; Schema: vibetype; Owner: ci -- @@ -3313,17 +3498,16 @@ Reference to the uploaded file.'; CREATE TABLE vibetype.friendship ( id uuid DEFAULT gen_random_uuid() NOT NULL, - a_account_id uuid NOT NULL, - b_account_id uuid NOT NULL, + account_id uuid NOT NULL, + friend_account_id uuid NOT NULL, + is_close_friend boolean DEFAULT false NOT NULL, status vibetype.friendship_status DEFAULT 'requested'::vibetype.friendship_status NOT NULL, created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, created_by uuid NOT NULL, updated_at timestamp with time zone, updated_by uuid, - CONSTRAINT friendship_creator_participant CHECK (((created_by = a_account_id) OR (created_by = b_account_id))), - CONSTRAINT friendship_creator_updater_difference CHECK ((created_by <> updated_by)), - CONSTRAINT friendship_ordering CHECK ((a_account_id < b_account_id)), - CONSTRAINT friendship_updater_participant CHECK (((updated_by IS NULL) OR (updated_by = a_account_id) OR (updated_by = b_account_id))) + CONSTRAINT friendship_creator_friend CHECK ((account_id <> friend_account_id)), + CONSTRAINT friendship_creator_participant CHECK ((created_by = account_id)) ); @@ -3345,19 +3529,27 @@ The friend relation''s internal id.'; -- --- Name: COLUMN friendship.a_account_id; Type: COMMENT; Schema: vibetype; Owner: ci +-- Name: COLUMN friendship.account_id; Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON COLUMN vibetype.friendship.account_id IS '@omit update +The one side of the friend relation. If the status is ''requested'' then it is the requestor account.'; + + +-- +-- Name: COLUMN friendship.friend_account_id; Type: COMMENT; Schema: vibetype; Owner: ci -- -COMMENT ON COLUMN vibetype.friendship.a_account_id IS '@omit update -The ''left'' side of the friend relation. It must be lexically less than the ''right'' side.'; +COMMENT ON COLUMN vibetype.friendship.friend_account_id IS '@omit update +The other side of the friend relation. If the status is ''requested'' then it is the requestee account.'; -- --- Name: COLUMN friendship.b_account_id; Type: COMMENT; Schema: vibetype; Owner: ci +-- Name: COLUMN friendship.is_close_friend; Type: COMMENT; Schema: vibetype; Owner: ci -- -COMMENT ON COLUMN vibetype.friendship.b_account_id IS '@omit update -The ''right'' side of the friend relation. It must be lexically greater than the ''left'' side.'; +COMMENT ON COLUMN vibetype.friendship.is_close_friend IS '@omit create +The flag indicating whether account_id considers friend_account_id as a close friend or not.'; -- @@ -4604,11 +4796,11 @@ ALTER TABLE ONLY vibetype.event_upload -- --- Name: friendship friendship_a_account_id_b_account_id_key; Type: CONSTRAINT; Schema: vibetype; Owner: ci +-- Name: friendship friendship_account_id_friend_account_id_key; Type: CONSTRAINT; Schema: vibetype; Owner: ci -- ALTER TABLE ONLY vibetype.friendship - ADD CONSTRAINT friendship_a_account_id_b_account_id_key UNIQUE (a_account_id, b_account_id); + ADD CONSTRAINT friendship_account_id_friend_account_id_key UNIQUE (account_id, friend_account_id); -- @@ -5281,27 +5473,27 @@ ALTER TABLE ONLY vibetype.event_upload -- --- Name: friendship friendship_a_account_id_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci +-- Name: friendship friendship_account_id_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci -- ALTER TABLE ONLY vibetype.friendship - ADD CONSTRAINT friendship_a_account_id_fkey FOREIGN KEY (a_account_id) REFERENCES vibetype.account(id) ON DELETE CASCADE; + ADD CONSTRAINT friendship_account_id_fkey FOREIGN KEY (account_id) REFERENCES vibetype.account(id) ON DELETE CASCADE; -- --- Name: friendship friendship_b_account_id_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci +-- Name: friendship friendship_created_by_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci -- ALTER TABLE ONLY vibetype.friendship - ADD CONSTRAINT friendship_b_account_id_fkey FOREIGN KEY (b_account_id) REFERENCES vibetype.account(id) ON DELETE CASCADE; + ADD CONSTRAINT friendship_created_by_fkey FOREIGN KEY (created_by) REFERENCES vibetype.account(id) ON DELETE CASCADE; -- --- Name: friendship friendship_created_by_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci +-- Name: friendship friendship_friend_account_id_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci -- ALTER TABLE ONLY vibetype.friendship - ADD CONSTRAINT friendship_created_by_fkey FOREIGN KEY (created_by) REFERENCES vibetype.account(id) ON DELETE CASCADE; + ADD CONSTRAINT friendship_friend_account_id_fkey FOREIGN KEY (friend_account_id) REFERENCES vibetype.account(id) ON DELETE CASCADE; -- @@ -5750,12 +5942,10 @@ CREATE POLICY event_upload_select ON vibetype.event_upload FOR SELECT USING ((ev ALTER TABLE vibetype.friendship ENABLE ROW LEVEL SECURITY; -- --- Name: friendship friendship_existing; Type: POLICY; Schema: vibetype; Owner: ci +-- Name: friendship friendship_delete; Type: POLICY; Schema: vibetype; Owner: ci -- -CREATE POLICY friendship_existing ON vibetype.friendship USING ((((vibetype.invoker_account_id() = a_account_id) AND (NOT (b_account_id IN ( SELECT account_block_ids.id - FROM vibetype_private.account_block_ids() account_block_ids(id))))) OR ((vibetype.invoker_account_id() = b_account_id) AND (NOT (a_account_id IN ( SELECT account_block_ids.id - FROM vibetype_private.account_block_ids() account_block_ids(id))))))) WITH CHECK (false); +CREATE POLICY friendship_delete ON vibetype.friendship FOR DELETE USING (true); -- @@ -5766,10 +5956,33 @@ CREATE POLICY friendship_insert ON vibetype.friendship FOR INSERT WITH CHECK ((c -- --- Name: friendship friendship_update; Type: POLICY; Schema: vibetype; Owner: ci +-- Name: friendship friendship_not_blocked; Type: POLICY; Schema: vibetype; Owner: ci -- -CREATE POLICY friendship_update ON vibetype.friendship FOR UPDATE USING ((status = 'requested'::vibetype.friendship_status)) WITH CHECK (((status = 'accepted'::vibetype.friendship_status) AND (updated_by = vibetype.invoker_account_id()))); +CREATE POLICY friendship_not_blocked ON vibetype.friendship USING (((NOT (account_id IN ( SELECT account_block_ids.id + FROM vibetype_private.account_block_ids() account_block_ids(id)))) AND (NOT (friend_account_id IN ( SELECT account_block_ids.id + FROM vibetype_private.account_block_ids() account_block_ids(id)))))); + + +-- +-- Name: friendship friendship_select; Type: POLICY; Schema: vibetype; Owner: ci +-- + +CREATE POLICY friendship_select ON vibetype.friendship FOR SELECT USING (((account_id = vibetype.invoker_account_id()) OR (friend_account_id = vibetype.invoker_account_id()))); + + +-- +-- Name: friendship friendship_update_accept; Type: POLICY; Schema: vibetype; Owner: ci +-- + +CREATE POLICY friendship_update_accept ON vibetype.friendship FOR UPDATE USING (((friend_account_id = vibetype.invoker_account_id()) AND (status = 'requested'::vibetype.friendship_status))) WITH CHECK (((status = 'accepted'::vibetype.friendship_status) AND (updated_by = vibetype.invoker_account_id()))); + + +-- +-- Name: friendship friendship_update_toggle_closeness; Type: POLICY; Schema: vibetype; Owner: ci +-- + +CREATE POLICY friendship_update_toggle_closeness ON vibetype.friendship FOR UPDATE USING (((status = 'accepted'::vibetype.friendship_status) AND (account_id = vibetype.invoker_account_id()))) WITH CHECK (((status = 'accepted'::vibetype.friendship_status) AND (updated_by = vibetype.invoker_account_id()))); -- @@ -6197,6 +6410,46 @@ GRANT ALL ON FUNCTION vibetype.events_organized() TO vibetype_account; GRANT ALL ON FUNCTION vibetype.events_organized() TO vibetype_anonymous; +-- +-- Name: FUNCTION friendship_accept(requestor_account_id uuid); Type: ACL; Schema: vibetype; Owner: ci +-- + +REVOKE ALL ON FUNCTION vibetype.friendship_accept(requestor_account_id uuid) FROM PUBLIC; +GRANT ALL ON FUNCTION vibetype.friendship_accept(requestor_account_id uuid) TO vibetype_account; + + +-- +-- Name: FUNCTION friendship_cancel(friend_account_id uuid); Type: ACL; Schema: vibetype; Owner: ci +-- + +REVOKE ALL ON FUNCTION vibetype.friendship_cancel(friend_account_id uuid) FROM PUBLIC; +GRANT ALL ON FUNCTION vibetype.friendship_cancel(friend_account_id uuid) TO vibetype_account; + + +-- +-- Name: FUNCTION friendship_notify_request(friend_account_id uuid, language text); Type: ACL; Schema: vibetype; Owner: ci +-- + +REVOKE ALL ON FUNCTION vibetype.friendship_notify_request(friend_account_id uuid, language text) FROM PUBLIC; +GRANT ALL ON FUNCTION vibetype.friendship_notify_request(friend_account_id uuid, language text) TO vibetype_account; + + +-- +-- Name: FUNCTION friendship_request(friend_account_id uuid, language text); Type: ACL; Schema: vibetype; Owner: ci +-- + +REVOKE ALL ON FUNCTION vibetype.friendship_request(friend_account_id uuid, language text) FROM PUBLIC; +GRANT ALL ON FUNCTION vibetype.friendship_request(friend_account_id uuid, language text) TO vibetype_account; + + +-- +-- Name: FUNCTION friendship_toggle_closeness(friend_account_id uuid); Type: ACL; Schema: vibetype; Owner: ci +-- + +REVOKE ALL ON FUNCTION vibetype.friendship_toggle_closeness(friend_account_id uuid) FROM PUBLIC; +GRANT ALL ON FUNCTION vibetype.friendship_toggle_closeness(friend_account_id uuid) TO vibetype_account; + + -- -- Name: FUNCTION guest_claim_array(); Type: ACL; Schema: vibetype; Owner: ci -- diff --git a/test/logic/scenario/model/friendship.sql b/test/logic/scenario/model/friendship.sql index b6b6d358..4ced4c97 100644 --- a/test/logic/scenario/model/friendship.sql +++ b/test/logic/scenario/model/friendship.sql @@ -7,9 +7,7 @@ DECLARE accountA UUID; accountB UUID; accountC UUID; - friendshipAB UUID; - friendshipAC UUID; - friendshipCA UUID; + rec RECORD; BEGIN -- before all accountA := vibetype_test.account_registration_verified('username-a', 'email+a@example.com'); @@ -17,41 +15,81 @@ BEGIN accountC := vibetype_test.account_registration_verified('username-c', 'email+c@example.com'); -- friendship request from user A to B - friendshipAB := vibetype_test.friendship_request(accountA, accountB); - PERFORM vibetype_test.friendship_test('The friendship is requested for user A', accountA, 'requested', ARRAY[friendshipAB]::UUID[]); - PERFORM vibetype_test.friendship_test('The friendship is requested for user B', accountB, 'requested', ARRAY[friendshipAB]::UUID[]); - PERFORM vibetype_test.friendship_account_ids_test('User A has no friends', accountA, ARRAY[]::UUID[]); - PERFORM vibetype_test.friendship_account_ids_test('User B has no friends', accountB, ARRAY[]::UUID[]); - - -- friendship acceptance - PERFORM vibetype_test.friendship_accept(accountB, friendshipAB); - PERFORM vibetype_test.friendship_test('The friendship is accepted for user A', accountA, 'accepted', ARRAY[friendshipAB]::UUID[]); - PERFORM vibetype_test.friendship_test('The friendship is accepted for user B', accountB, 'accepted', ARRAY[friendshipAB]::UUID[]); - PERFORM vibetype_test.friendship_test('There is no requested friendship for user A', accountA, 'requested', ARRAY[]::UUID[]); - PERFORM vibetype_test.friendship_test('There is no requested friendship for user B', accountA, 'requested', ARRAY[]::UUID[]); - PERFORM vibetype_test.friendship_account_ids_test('User B is a friend of user A', accountA, ARRAY[accountB]::UUID[]); - PERFORM vibetype_test.friendship_account_ids_test('User A is a friend of user B', accountB, ARRAY[accountA]::UUID[]); + PERFORM vibetype_test.friendship_request(accountA, accountB, 'de'); + + + RAISE NOTICE '----'; + FOR rec IN + SELECT a.username, b.username as friend_username, f.is_close_friend, f.status + FROM vibetype.friendship f + JOIN vibetype.account a ON f.account_id = a.id + JOIN vibetype.account b ON f.friend_account_id = b.id + LOOP + RAISE NOTICE 'friendship: account = %, friend_account = %, is_close_friend = %, status = %', rec.username, rec.friend_username, rec.is_close_friend, rec.status; + END LOOP; + + PERFORM vibetype_test.friendship_test('A sends B a friendship request (1)', accountA, accountB, false, 'requested', 1); + PERFORM vibetype_test.friendship_test('A sends B a friendship request (2)', accountB, accountA, false, 'requested', 0); + PERFORM vibetype_test.friendship_test('A sends B a friendship request (3)', accountA, accountB, false, null, 1); + PERFORM vibetype_test.friendship_test('A sends B a friendship request (3)', accountB, accountA, false, null, 0); + + -- B accepts A's friendship request + PERFORM vibetype_test.friendship_accept(accountB, accountA); + + PERFORM vibetype_test.friendship_test('B accepts friendship request from A (1)', accountA, accountB, false, 'requested', 0); + PERFORM vibetype_test.friendship_test('B accepts friendship request from A (2)', accountA, accountB, false, 'accepted', 1); + PERFORM vibetype_test.friendship_test('B accepts friendship request from A (3)', accountB, accountA, false, 'accepted', 1); -- friendship request from user C to A - friendshipCA := vibetype_test.friendship_request(accountC, accountA); - PERFORM vibetype_test.friendship_test('There is still only one accepted friendship for user A', accountA, 'accepted', ARRAY[friendshipAB]::UUID[]); - PERFORM vibetype_test.friendship_test('There is a new requested friendship for user C', accountC, 'requested', ARRAY[friendshipCA]::UUID[]); - PERFORM vibetype_test.friendship_account_ids_test('User B is still a friend of user A', accountA, ARRAY[accountB]::UUID[]); - PERFORM vibetype_test.friendship_account_ids_test('User C has no friends', accountC, ARRAY[]::UUID[]); + PERFORM vibetype_test.friendship_request(accountC, accountA, 'de'); + + PERFORM vibetype_test.friendship_test('There is still only one accepted friendship for user A', accountA, null, false, 'accepted', 1); + PERFORM vibetype_test.friendship_test('There is a new requested friendship for user C (1)', accountC, null, false, 'requested', 1); + PERFORM vibetype_test.friendship_test('There is a new requested friendship for user C (2)', accountA, accountC, false, null, 0); + PERFORM vibetype_test.friendship_test('User B is still a friend of user A (1)', accountA, accountB, false, null, 1); + PERFORM vibetype_test.friendship_test('User B is still a friend of user A (2)', accountB, accountA, false, null, 1); + PERFORM vibetype_test.friendship_test('User C has no friends', accountC, null, false, 'accepted', 0); BEGIN - friendshipAC := vibetype_test.friendship_request(accountA, accountC); - RAISE 'It was possible to requested a friendship more than once.'; + PERFORM vibetype_test.friendship_request(accountA, accountC, 'de'); + RAISE 'It was possible to request a friendship more than once.'; EXCEPTION - WHEN unique_violation THEN -- do nothing as expected - WHEN OTHERS THEN RAISE; + WHEN OTHERS THEN + IF SQLSTATE != 'VTREQ' THEN + RAISE; + END IF; END; -- friendship rejection - PERFORM vibetype_test.friendship_reject(accountA, friendshipCA); - PERFORM vibetype_test.friendship_test('After user A rejected user C''s friendship request, the friendship is removed for user C', accountC, NULL, ARRAY[]::UUID[]); - PERFORM vibetype_test.friendship_account_ids_test('After user A rejected user C''s friendship request, user B is still a friend of user A', accountA, ARRAY[accountB]::UUID[]); - PERFORM vibetype_test.friendship_account_ids_test('After user A rejected user C''s friendship request, user C has no friends anymore', accountC, ARRAY[]::UUID[]); + PERFORM vibetype_test.friendship_cancel(accountA, accountC); + PERFORM vibetype_test.friendship_test('After user A rejected user C''s friendship request (1)', accountC, accountA, false, null, 0); + PERFORM vibetype_test.friendship_test('After user A rejected user C''s friendship request (2)', accountA, accountC, false, null, 0); + + -- a new friendship request from user C to A, this time accepted by A + PERFORM vibetype_test.friendship_request(accountC, accountA, 'de'); + PERFORM vibetype_test.friendship_accept(accountA, accountC); + PERFORM vibetype_test.friendship_test('Count the number of A''s friends', accountA, null, false, 'accepted', 2); + PERFORM vibetype_test.friendship_test('C is a friend of A (1)', accountA, accountC, false, 'accepted', 1); + PERFORM vibetype_test.friendship_test('C is a friend of A (2)', accountC, accountA, false, 'accepted', 1); + + -- friendship request from user B to A + PERFORM vibetype_test.friendship_request(accountB, accountC, 'de'); + + BEGIN + PERFORM vibetype_test.friendship_toggle_closeness(accountB, accountC); + RAISE 'It was possible to toggle closeness in a friendship request.'; + EXCEPTION + WHEN OTHERS THEN + IF SQLSTATE != 'VTFTC' THEN + RAISE; + END IF; + END; + + -- B marks A as a close friend + PERFORM vibetype_test.friendship_toggle_closeness(accountB, accountA); + PERFORM vibetype_test.friendship_test('B marks A as a close friend (1)', accountB, accountA, true, 'accepted', 1); + PERFORM vibetype_test.friendship_test('B marks A as a close friend (2)', accountA, accountB, true, NULL, 0); + END $$; ROLLBACK; diff --git a/test/logic/utility/model/account_registration.sql b/test/logic/utility/model/account_registration.sql index 19d943f9..5741a410 100644 --- a/test/logic/utility/model/account_registration.sql +++ b/test/logic/utility/model/account_registration.sql @@ -8,7 +8,7 @@ DECLARE _verification UUID; BEGIN _legal_term_id := vibetype_test.legal_term_select_by_singleton(); - PERFORM vibetype.account_registration('1970-01-01', _email_address, 'en', _legal_term_id, 'password', _username); + PERFORM vibetype.account_registration(to_date('1970-01-01', 'yyyy-mm-dd'), _email_address, 'en', _legal_term_id, 'password', _username); SELECT id INTO _account_id FROM vibetype.account diff --git a/test/logic/utility/model/friendship.sql b/test/logic/utility/model/friendship.sql index 78337634..c406ae6d 100644 --- a/test/logic/utility/model/friendship.sql +++ b/test/logic/utility/model/friendship.sql @@ -1,6 +1,6 @@ CREATE OR REPLACE FUNCTION vibetype_test.friendship_accept ( _invoker_account_id UUID, - _id UUID + _requestor_account_id UUID ) RETURNS VOID AS $$ DECLARE rec RECORD; @@ -9,9 +9,7 @@ BEGIN SET LOCAL role = 'vibetype_account'; EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || _invoker_account_id || ''''; - UPDATE vibetype.friendship - SET "status" = 'accepted'::vibetype.friendship_status - WHERE id = _id; + PERFORM vibetype.friendship_accept(_requestor_account_id); SET LOCAL ROLE NONE; END $$ LANGUAGE plpgsql; @@ -19,149 +17,80 @@ END $$ LANGUAGE plpgsql; GRANT EXECUTE ON FUNCTION vibetype_test.friendship_accept(UUID, UUID) TO vibetype_account; -CREATE OR REPLACE FUNCTION vibetype_test.friendship_reject ( +CREATE OR REPLACE FUNCTION vibetype_test.friendship_cancel ( _invoker_account_id UUID, - _id UUID + _friend_account_id UUID ) RETURNS VOID AS $$ BEGIN SET LOCAL role = 'vibetype_account'; EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || _invoker_account_id || ''''; - DELETE FROM vibetype.friendship - WHERE id = _id; + PERFORM vibetype.friendship_cancel(_friend_account_id); SET LOCAL ROLE NONE; END $$ LANGUAGE plpgsql; -GRANT EXECUTE ON FUNCTION vibetype_test.friendship_reject(UUID, UUID) TO vibetype_account; +GRANT EXECUTE ON FUNCTION vibetype_test.friendship_cancel(UUID, UUID) TO vibetype_account; CREATE OR REPLACE FUNCTION vibetype_test.friendship_request ( _invoker_account_id UUID, - _friend_account_id UUID -) RETURNS UUID AS $$ -DECLARE - _id UUID; - _a_account_id UUID; - _b_account_id UUID; + _friend_account_id UUID, + _language TEXT +) RETURNS VOID AS $$ BEGIN SET LOCAL role = 'vibetype_account'; EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || _invoker_account_id || ''''; - IF _invoker_account_id < _friend_account_id THEN - _a_account_id := _invoker_account_id; - _b_account_id := _friend_account_id; - ELSE - _a_account_id := _friend_account_id; - _b_account_id := _invoker_account_id; - END IF; - - INSERT INTO vibetype.friendship(a_account_id, b_account_id, created_by) - VALUES (_a_account_id, _b_account_id, _invoker_account_id) - RETURNING id INTO _id; + PERFORM vibetype.friendship_request(_friend_account_id, _language); SET LOCAL ROLE NONE; - - RETURN _id; END $$ LANGUAGE plpgsql; -GRANT EXECUTE ON FUNCTION vibetype_test.friendship_request(UUID, UUID) TO vibetype_account; +GRANT EXECUTE ON FUNCTION vibetype_test.friendship_request(UUID, UUID, TEXT) TO vibetype_account; - -CREATE OR REPLACE FUNCTION vibetype_test.friendship_test ( - _test_case TEXT, +CREATE OR REPLACE FUNCTION vibetype_test.friendship_toggle_closeness ( _invoker_account_id UUID, - _status TEXT, -- status IS NULL means "any status" - _expected_result UUID[] + _friend_account_id UUID ) RETURNS VOID AS $$ -DECLARE - rec RECORD; BEGIN - IF _invoker_account_id IS NULL THEN - SET LOCAL role = 'vibetype_anonymous'; - SET LOCAL jwt.claims.account_id = ''; - ELSE - SET LOCAL role = 'vibetype_account'; - EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || _invoker_account_id || ''''; - END IF; + SET LOCAL role = 'vibetype_account'; + EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || _invoker_account_id || ''''; - IF EXISTS ( - SELECT id FROM vibetype.friendship WHERE _status IS NULL OR status = _status::vibetype.friendship_status - EXCEPT - SELECT * FROM unnest(_expected_result) - ) THEN - RAISE EXCEPTION 'some accounts should not appear in the query result'; - END IF; - - IF EXISTS ( - SELECT * FROM unnest(_expected_result) - EXCEPT - SELECT id FROM vibetype.friendship WHERE _status IS NULL OR status = _status::vibetype.friendship_status - ) THEN - RAISE EXCEPTION 'some account is missing in the query result'; - END IF; + PERFORM vibetype.friendship_toggle_closeness(_friend_account_id); SET LOCAL ROLE NONE; END $$ LANGUAGE plpgsql; -GRANT EXECUTE ON FUNCTION vibetype_test.friendship_test(TEXT, UUID, TEXT, UUID[]) TO vibetype_account; +GRANT EXECUTE ON FUNCTION vibetype_test.friendship_toggle_closeness(UUID, UUID) TO vibetype_account; -CREATE OR REPLACE FUNCTION vibetype_test.friendship_account_ids_test ( +CREATE OR REPLACE FUNCTION vibetype_test.friendship_test ( _test_case TEXT, _invoker_account_id UUID, - _expected_result UUID[] + _friend_account_id UUID, + _is_close_friend BOOLEAN, -- _is_close_friend IS NULL means "any boolean value" + _status TEXT, -- _status IS NULL means "any status" + _expected_count INTEGER ) RETURNS VOID AS $$ DECLARE - rec RECORD; + _result INTEGER; BEGIN - IF _invoker_account_id IS NULL THEN - SET LOCAL jwt.claims.account_id = ''; - ELSE - EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || _invoker_account_id || ''''; - END IF; + SET LOCAL role = 'vibetype_account'; + EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || _invoker_account_id || ''''; - IF EXISTS ( - WITH friendship_account_ids_test AS ( - SELECT b_account_id as account_id - FROM vibetype.friendship - WHERE a_account_id = _invoker_account_id - and status = 'accepted'::vibetype.friendship_status - UNION ALL - SELECT a_account_id as account_id - FROM vibetype.friendship - WHERE b_account_id = _invoker_account_id - and status = 'accepted'::vibetype.friendship_status - ) - SELECT account_id as id - FROM friendship_account_ids_test - WHERE account_id NOT IN (SELECT b.id FROM vibetype_private.account_block_ids() b) - EXCEPT - SELECT * FROM unnest(_expected_result) - ) THEN - RAISE EXCEPTION 'some accounts should not appear in the list of friends'; - END IF; + SELECT count(*) INTO _result + FROM vibetype.friendship + WHERE account_id = _invoker_account_id + AND (_status IS NULL OR status = _status::vibetype.friendship_status) + AND (_is_close_friend IS NULL OR is_close_friend = _is_close_friend) + AND (_friend_account_id IS NULL OR friend_account_id = _friend_account_id); - IF EXISTS ( - WITH friendship_account_ids_test AS ( - SELECT b_account_id as account_id - FROM vibetype.friendship - WHERE a_account_id = vibetype.invoker_account_id() - and status = 'accepted'::vibetype.friendship_status - UNION ALL - SELECT a_account_id as account_id - FROM vibetype.friendship - WHERE b_account_id = vibetype.invoker_account_id() - and status = 'accepted'::vibetype.friendship_status - ) - SELECT * FROM unnest(_expected_result) - EXCEPT - SELECT account_id as id - FROM friendship_account_ids_test - WHERE account_id NOT IN (SELECT b.id FROM vibetype_private.account_block_ids() b) - ) THEN - RAISE EXCEPTION 'some account is missing in the list of friends'; + IF _result != _expected_count THEN + RAISE EXCEPTION 'Expected count was % but result is %.', _expected_count, _result USING ERRCODE = 'VTTST'; END IF; -END $$ LANGUAGE plpgsql SECURITY DEFINER; -GRANT EXECUTE ON FUNCTION vibetype_test.friendship_account_ids_test(TEXT, UUID, UUID[]) TO vibetype_account; + SET LOCAL ROLE NONE; +END $$ LANGUAGE plpgsql; + +GRANT EXECUTE ON FUNCTION vibetype_test.friendship_test(TEXT, UUID, UUID, BOOLEAN, TEXT, INTEGER) TO vibetype_account; From 1e828b708cc078ea67e102ca151c7698786f9f9c Mon Sep 17 00:00:00 2001 From: Sven Thelemann Date: Thu, 7 Aug 2025 18:49:50 +0200 Subject: [PATCH 2/8] feat(friendship): new table friendship_request There is a new table `friendship_request` holding a single row per friendship request. If the request is accepted then 2 records will be inserted into table `friendship` and the row in `friendship_request` will be deleted. If the request is rejected then the row in `friendship_request`will also be deleted deleted. This makes the enum type `friendship_status` superfluous and also allows policies on table `friendship` in order to hide information about friendship closeness as set by other users. --- src/deploy/enum_friendship_status.sql | 11 - src/deploy/function_friendship.sql | 86 ++++--- src/deploy/table_friendship.sql | 98 +++++--- src/revert/enum_friendship_status.sql | 5 - src/revert/function_friendship.sql | 1 + src/revert/table_friendship.sql | 14 +- src/sqitch.plan | 3 +- src/verify/enum_friendship_status.sql | 8 - src/verify/function_friendship.sql | 4 + src/verify/table_friendship.sql | 24 +- test/fixture/schema_vibetype.definition.sql | 248 +++++++++++++++----- test/logic/scenario/model/friendship.sql | 142 +++++++---- test/logic/utility/model/friendship.sql | 59 ++++- 13 files changed, 506 insertions(+), 197 deletions(-) delete mode 100644 src/deploy/enum_friendship_status.sql delete mode 100644 src/revert/enum_friendship_status.sql delete mode 100644 src/verify/enum_friendship_status.sql diff --git a/src/deploy/enum_friendship_status.sql b/src/deploy/enum_friendship_status.sql deleted file mode 100644 index fa585a6c..00000000 --- a/src/deploy/enum_friendship_status.sql +++ /dev/null @@ -1,11 +0,0 @@ -BEGIN; - -CREATE TYPE vibetype.friendship_status AS ENUM ( - 'accepted', - 'requested' -); - -COMMENT ON TYPE vibetype.friendship_status IS 'Possible status values of a friend relation. -There is no status `rejected` because friendship records will be deleted when a friendship request is rejected.'; - -COMMIT; diff --git a/src/deploy/function_friendship.sql b/src/deploy/function_friendship.sql index 14d7137d..1539be43 100644 --- a/src/deploy/function_friendship.sql +++ b/src/deploy/function_friendship.sql @@ -2,39 +2,42 @@ BEGIN; -- accept friendship request -CREATE OR REPLACE FUNCTION vibetype.friendship_accept( +CREATE FUNCTION vibetype.friendship_accept( requestor_account_id UUID ) RETURNS VOID AS $$ DECLARE _friend_account_id UUID; - _count INTEGER; + _id UUID; BEGIN _friend_account_id := vibetype.invoker_account_id(); - UPDATE vibetype.friendship SET - status = 'accepted'::vibetype.friendship_status - -- updated_by filled by trigger - WHERE account_id = requestor_account_id AND friend_account_id = _friend_account_id - AND status = 'requested'::vibetype.friendship_status; + SELECT id INTO _id + FROM vibetype.friendship_request + WHERE account_id = requestor_account_id AND friend_account_id = _friend_account_id; - GET DIAGNOSTICS _count = ROW_COUNT; - IF _count = 0 THEN + IF _id IS NULL THEN RAISE EXCEPTION 'Friendship request does not exist' USING ERRCODE = 'VTFAC'; END IF; - INSERT INTO vibetype.friendship(account_id, friend_account_id, status, created_by) - VALUES (_friend_account_id, requestor_account_id, 'accepted'::vibetype.friendship_status, _friend_account_id); + INSERT INTO vibetype.friendship(account_id, friend_account_id, created_by) + VALUES (requestor_account_id, _friend_account_id, requestor_account_id); + + INSERT INTO vibetype.friendship(account_id, friend_account_id, created_by) + VALUES (_friend_account_id, requestor_account_id, _friend_account_id); + + DELETE FROM vibetype.friendship_request + WHERE account_id = requestor_account_id AND friend_account_id = vibetype.invoker_account_id(); END; $$ LANGUAGE plpgsql SECURITY INVOKER; -COMMENT ON FUNCTION vibetype.friendship_accept(UUID) IS 'Accepts a friendship request.\n\nError codes:\n- **VTFAC** when a corresponding friendship request does not exist.'; +COMMENT ON FUNCTION vibetype.friendship_accept(UUID) IS E'Accepts a friendship request.\n\nError codes:\n- **VTFAC** when a corresponding friendship request does not exist.'; GRANT EXECUTE ON FUNCTION vibetype.friendship_accept(UUID) TO vibetype_account; --- reject or cancel friendship +-- cancel friendship -CREATE OR REPLACE FUNCTION vibetype.friendship_cancel( +CREATE FUNCTION vibetype.friendship_cancel( friend_account_id UUID ) RETURNS VOID AS $$ DECLARE @@ -49,13 +52,13 @@ BEGIN END; $$ LANGUAGE plpgsql SECURITY INVOKER; -COMMENT ON FUNCTION vibetype.friendship_cancel(UUID) IS 'Rejects or cancels a friendship (in both directions).'; +COMMENT ON FUNCTION vibetype.friendship_cancel(UUID) IS 'Cancels a friendship (in both directions) if it exists.'; GRANT EXECUTE ON FUNCTION vibetype.friendship_cancel(UUID) TO vibetype_account; -- create notification for a request -CREATE OR REPLACE FUNCTION vibetype.friendship_notify_request( +CREATE FUNCTION vibetype.friendship_notify_request( friend_account_id UUID, language TEXT ) RETURNS VOID AS $$ @@ -79,9 +82,25 @@ COMMENT ON FUNCTION vibetype.friendship_notify_request(UUID, TEXT) IS 'Creates a GRANT EXECUTE ON FUNCTION vibetype.friendship_notify_request(UUID, TEXT) TO vibetype_account; +-- reject friendship request + +CREATE FUNCTION vibetype.friendship_reject( + requestor_account_id UUID +) RETURNS VOID AS $$ +BEGIN + + DELETE FROM vibetype.friendship_request + WHERE account_id = requestor_account_id AND friend_account_id = vibetype.invoker_account_id(); + +END; $$ LANGUAGE plpgsql SECURITY DEFINER; + +COMMENT ON FUNCTION vibetype.friendship_reject(UUID) IS 'Rejects a friendship request'; + +GRANT EXECUTE ON FUNCTION vibetype.friendship_reject(UUID) TO vibetype_account; + -- request friendship -CREATE OR REPLACE FUNCTION vibetype.friendship_request( +CREATE FUNCTION vibetype.friendship_request( friend_account_id UUID, language TEXT ) RETURNS VOID AS $$ @@ -95,42 +114,53 @@ BEGIN SELECT 1 FROM vibetype.friendship f WHERE (f.account_id = _account_id AND f.friend_account_id = friendship_request.friend_account_id) - OR (f.account_id = friendship_request.friend_account_id AND f.friend_account_id = _account_id) ) THEN - RAISE EXCEPTION 'Friendship already exists or has already been requested.' USING ERRCODE = 'VTREQ'; + RAISE EXCEPTION 'Friendship already exists.' USING ERRCODE = 'VTFEX'; + END IF; + + IF EXISTS( + SELECT 1 + FROM vibetype.friendship_request r + WHERE (r.account_id = _account_id AND r.friend_account_id = friendship_request.friend_account_id) + OR (r.account_id = friendship_request.friend_account_id AND r.friend_account_id = _account_id) + ) + THEN + RAISE EXCEPTION 'There is already a friendship request.' USING ERRCODE = 'VTREQ'; END IF; - INSERT INTO vibetype.friendship(account_id, friend_account_id, status, created_by) - VALUES (_account_id, friendship_request.friend_account_id, 'requested'::vibetype.friendship_status, _account_id); + INSERT INTO vibetype.friendship_request(account_id, friend_account_id, created_by) + VALUES (_account_id, friendship_request.friend_account_id, _account_id); PERFORM vibetype.friendship_notify_request(friendship_request.friend_account_id, friendship_request.language); END; $$ LANGUAGE plpgsql SECURITY INVOKER; -COMMENT ON FUNCTION vibetype.friendship_request(UUID, TEXT) IS 'Starts a new friendship request.\n\nError codes:\n- **VTREQ** when the friendship already exists or has already been requested.'; +COMMENT ON FUNCTION vibetype.friendship_request(UUID, TEXT) IS E'Starts a new friendship request.\n\nError codes:\n- **VTFEX** when the friendship already exists.\n- **VTREQ** when there is already a friendship request.'; GRANT EXECUTE ON FUNCTION vibetype.friendship_request(UUID, TEXT) TO vibetype_account; -- toggle closeness of friendship -CREATE OR REPLACE FUNCTION vibetype.friendship_toggle_closeness( +CREATE FUNCTION vibetype.friendship_toggle_closeness( friend_account_id UUID ) RETURNS BOOLEAN AS $$ DECLARE _account_id UUID; + _id UUID; _is_close_friend BOOLEAN; - current_status vibetype.friendship_status; BEGIN _account_id := vibetype.invoker_account_id(); - SELECT status INTO current_status + SELECT f.id + INTO _id FROM vibetype.friendship f - WHERE f.account_id = _account_id AND f.friend_account_id = friendship_toggle_closeness.friend_account_id; + WHERE f.account_id = _account_id + AND f.friend_account_id = friendship_toggle_closeness.friend_account_id; - IF current_status IS NULL OR current_status != 'accepted'::vibetype.friendship_status THEN + IF _id IS NULL THEN RAISE EXCEPTION 'Friendship does not exist' USING ERRCODE = 'VTFTC'; END IF; @@ -144,7 +174,7 @@ BEGIN END; $$ LANGUAGE plpgsql SECURITY INVOKER; -COMMENT ON FUNCTION vibetype.friendship_toggle_closeness(UUID) IS 'Toggles a frien1dship relation between ''not a close friend'' and ''close friend''.\n\nError codes:\n- **VTFTC** when the friendship does not exist.'; +COMMENT ON FUNCTION vibetype.friendship_toggle_closeness(UUID) IS E'Toggles a friendship relation between ''not a close friend'' and ''close friend''.\n\nError codes:\n- **VTFTC** when the friendship does not exist.'; GRANT EXECUTE ON FUNCTION vibetype.friendship_toggle_closeness(UUID) TO vibetype_account; diff --git a/src/deploy/table_friendship.sql b/src/deploy/table_friendship.sql index 2553f04c..eb6f47f4 100644 --- a/src/deploy/table_friendship.sql +++ b/src/deploy/table_friendship.sql @@ -1,11 +1,59 @@ +----------------------------------------------------------- +-- TABLE vibetype.friendship_request +----------------------------------------------------------- + +CREATE TABLE vibetype.friendship_request ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + account_id UUID NOT NULL REFERENCES vibetype.account(id) ON DELETE CASCADE, + friend_account_id UUID NOT NULL REFERENCES vibetype.account(id) ON DELETE CASCADE, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_by UUID NOT NULL REFERENCES vibetype.account(id) ON DELETE CASCADE, + + UNIQUE (account_id, friend_account_id), + CONSTRAINT friendship_creator_friend CHECK (account_id <> friend_account_id), + CONSTRAINT friendship_creator_participant CHECK (created_by = account_id) +); + +GRANT SELECT, INSERT, DELETE ON TABLE vibetype.friendship_request TO vibetype_account; + +ALTER TABLE vibetype.friendship_request ENABLE ROW LEVEL SECURITY; + +CREATE POLICY friendship_request_not_blocked ON vibetype.friendship_request FOR ALL +USING ( + account_id NOT IN (SELECT id FROM vibetype_private.account_block_ids()) + AND friend_account_id NOT IN (SELECT id FROM vibetype_private.account_block_ids()) +); + +-- Only allow interactions with friendships in which the current user is involved. +CREATE POLICY friendship_request_select ON vibetype.friendship_request FOR SELECT +USING ( + account_id = vibetype.invoker_account_id() + OR + friend_account_id = vibetype.invoker_account_id() +); + +-- Only allow creation by the current user. +CREATE POLICY friendship_request_insert ON vibetype.friendship_request FOR INSERT +WITH CHECK ( + created_by = vibetype.invoker_account_id() +); + +CREATE POLICY friendship_request_delete ON vibetype.friendship_request FOR DELETE +USING ( + friend_account_id = vibetype.invoker_account_id() +); + +----------------------------------------------------------- +-- TABLE vibetype.friendship +----------------------------------------------------------- + CREATE TABLE vibetype.friendship ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), account_id UUID NOT NULL REFERENCES vibetype.account(id) ON DELETE CASCADE, friend_account_id UUID NOT NULL REFERENCES vibetype.account(id) ON DELETE CASCADE, - is_close_friend BOOLEAN NOT NULL DEFAULT false, - status vibetype.friendship_status NOT NULL DEFAULT 'requested'::vibetype.friendship_status, + is_close_friend BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID NOT NULL REFERENCES vibetype.account(id) ON DELETE CASCADE, @@ -25,7 +73,6 @@ COMMENT ON COLUMN vibetype.friendship.id IS E'@omit create,update\nThe friend re COMMENT ON COLUMN vibetype.friendship.account_id IS E'@omit update\nThe one side of the friend relation. If the status is ''requested'' then it is the requestor account.'; COMMENT ON COLUMN vibetype.friendship.friend_account_id IS E'@omit update\nThe other side of the friend relation. If the status is ''requested'' then it is the requestee account.'; COMMENT ON COLUMN vibetype.friendship.is_close_friend IS E'@omit create\nThe flag indicating whether account_id considers friend_account_id as a close friend or not.'; -COMMENT ON COLUMN vibetype.friendship.status IS E'@omit create\nThe status of the friend relation.'; COMMENT ON COLUMN vibetype.friendship.created_at IS E'@omit create,update\nThe timestamp when the friend relation was created.'; COMMENT ON COLUMN vibetype.friendship.created_by IS E'@omit update\nThe account that created the friend relation was created.'; COMMENT ON COLUMN vibetype.friendship.updated_at IS E'@omit create,update\nThe timestamp when the friend relation''s status was updated.'; @@ -40,11 +87,11 @@ CREATE TRIGGER vibetype_trigger_friendship_update FOR EACH ROW EXECUTE PROCEDURE vibetype.trigger_metadata_update(); -GRANT INSERT, SELECT, UPDATE, DELETE ON TABLE vibetype.friendship TO vibetype_account; +GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE vibetype.friendship TO vibetype_account; ALTER TABLE vibetype.friendship ENABLE ROW LEVEL SECURITY; -CREATE POLICY friendship_not_blocked ON vibetype.friendship FOR ALL +CREATE POLICY friendship_not_blocked ON vibetype.friendship AS RESTRICTIVE FOR ALL USING ( account_id NOT IN (SELECT id FROM vibetype_private.account_block_ids()) AND friend_account_id NOT IN (SELECT id FROM vibetype_private.account_block_ids()) @@ -54,41 +101,36 @@ USING ( CREATE POLICY friendship_select ON vibetype.friendship FOR SELECT USING ( account_id = vibetype.invoker_account_id() - OR - friend_account_id = vibetype.invoker_account_id() ); --- Only allow creation by the current user. +-- Only allow creation by the current user and only if a friendship request is present. CREATE POLICY friendship_insert ON vibetype.friendship FOR INSERT WITH CHECK ( - created_by = vibetype.invoker_account_id() -); - --- Only allow update by the current user if it is about accepting a friendship request. -CREATE POLICY friendship_update_accept ON vibetype.friendship FOR UPDATE -USING ( - friend_account_id = vibetype.invoker_account_id() - AND - status = 'requested'::vibetype.friendship_status -) WITH CHECK ( - status = 'accepted'::vibetype.friendship_status - AND - updated_by = vibetype.invoker_account_id() + (account_id, friend_account_id, created_by) IN ( + SELECT account_id, friend_account_id, account_id + FROM vibetype.friendship_request + WHERE friend_account_id = vibetype.invoker_account_id() + ) + OR + (account_id, friend_account_id, created_by) IN ( + SELECT friend_account_id, account_id, friend_account_id + FROM vibetype.friendship_request + WHERE friend_account_id = vibetype.invoker_account_id() + ) ); --- Only allow update by the current user if it is already an accepted relation. -CREATE POLICY friendship_update_toggle_closeness ON vibetype.friendship FOR UPDATE +-- Only allow update by the current user. +CREATE POLICY friendship_update ON vibetype.friendship FOR UPDATE USING ( - status = 'accepted'::vibetype.friendship_status - AND account_id = vibetype.invoker_account_id() ) WITH CHECK ( - status = 'accepted'::vibetype.friendship_status - AND updated_by = vibetype.invoker_account_id() ); +-- Only allow deletion if the current user is involved in the friendship. CREATE POLICY friendship_delete ON vibetype.friendship FOR DELETE USING ( - TRUE + account_id = vibetype.invoker_account_id() + OR + friend_account_id = vibetype.invoker_account_id() ); diff --git a/src/revert/enum_friendship_status.sql b/src/revert/enum_friendship_status.sql deleted file mode 100644 index 04e22d38..00000000 --- a/src/revert/enum_friendship_status.sql +++ /dev/null @@ -1,5 +0,0 @@ -BEGIN; - -DROP TYPE vibetype.friendship_status; - -COMMIT; diff --git a/src/revert/function_friendship.sql b/src/revert/function_friendship.sql index db7c86a7..b02f5fdc 100644 --- a/src/revert/function_friendship.sql +++ b/src/revert/function_friendship.sql @@ -3,6 +3,7 @@ BEGIN; DROP FUNCTION vibetype.friendship_accept(UUID); DROP FUNCTION vibetype.friendship_cancel(UUID); DROP FUNCTION vibetype.friendship_notify_request(UUID, TEXT); +DROP FUNCTION vibetype.friendship_reject(UUID); DROP FUNCTION vibetype.friendship_request(UUID, TEXT); DROP FUNCTION vibetype.friendship_toggle_closeness(UUID); diff --git a/src/revert/table_friendship.sql b/src/revert/table_friendship.sql index 17413551..d55c1efa 100644 --- a/src/revert/table_friendship.sql +++ b/src/revert/table_friendship.sql @@ -1,10 +1,11 @@ BEGIN; +-- vibetype.friendship + DROP POLICY friendship_not_blocked ON vibetype.friendship; DROP POLICY friendship_select ON vibetype.friendship; DROP POLICY friendship_insert ON vibetype.friendship; -DROP POLICY friendship_update_accept ON vibetype.friendship; -DROP POLICY friendship_update_toggle_closeness ON vibetype.friendship; +DROP POLICY friendship_update ON vibetype.friendship; DROP POLICY friendship_delete ON vibetype.friendship; DROP TRIGGER vibetype_trigger_friendship_update ON vibetype.friendship; @@ -13,4 +14,13 @@ DROP INDEX vibetype.idx_friendship_updated_by; DROP INDEX vibetype.idx_friendship_created_by; DROP TABLE vibetype.friendship; +-- vibetype.friendship_request + +DROP POLICY friendship_request_not_blocked ON vibetype.friendship_request; +DROP POLICY friendship_request_select ON vibetype.friendship_request; +DROP POLICY friendship_request_insert ON vibetype.friendship_request; +DROP POLICY friendship_request_delete ON vibetype.friendship_request; + +DROP TABLE vibetype.friendship_request; + COMMIT; diff --git a/src/sqitch.plan b/src/sqitch.plan index 18ee931b..6cac1bd3 100644 --- a/src/sqitch.plan +++ b/src/sqitch.plan @@ -82,8 +82,7 @@ table_event_favorite [schema_public table_account_public table_event] 1970-01-01 function_guest_create_multiple [schema_public table_guest role_account] 1970-01-01T00:00:00Z Sven Thelemann # Function for inserting multiple guest records. function_event_search [privilege_execute_revoke schema_public enum_language schema_private function_language_iso_full_text_search table_event role_account role_anonymous] 1970-01-01T00:00:00Z Jonas Thelemann # Full-text search on events. table_device [schema_public table_account_public function_trigger_metadata_update] 1970-01-01T00:00:00Z Jonas Thelemann # A device that's assigned to an account. -enum_friendship_status [schema_public] 1970-01-01T00:00:00Z Sven Thelemann # Possible status values of a friend relation. -table_friendship [schema_public enum_friendship_status table_account_public function_trigger_metadata_update] 1970-01-01T00:00:00Z Sven Thelemann # A friend relation together with its status. +table_friendship [schema_public table_account_public function_trigger_metadata_update] 1970-01-01T00:00:00Z Sven Thelemann # A friend relation together with its status. table_event_format [schema_public role_anonymous role_account] 1970-01-01T00:00:00Z Jonas Thelemann # Table for storing event formats. table_event_format_mapping [schema_public table_event table_event_format role_anonymous role_account function_invoker_account_id] 1970-01-01T00:00:00Z Jonas Thelemann # Table for storing event to category (M:N) relationships. table_audit_log [schema_private] 1970-01-01T00:00:00Z Sven Thelemann # Table for storing audit log records. diff --git a/src/verify/enum_friendship_status.sql b/src/verify/enum_friendship_status.sql deleted file mode 100644 index 8bbb77c5..00000000 --- a/src/verify/enum_friendship_status.sql +++ /dev/null @@ -1,8 +0,0 @@ -BEGIN; - -DO $$ -BEGIN - ASSERT (SELECT pg_catalog.has_type_privilege('vibetype.friendship_status', 'USAGE')); -END $$; - -ROLLBACK; diff --git a/src/verify/function_friendship.sql b/src/verify/function_friendship.sql index 5a922070..24c7f462 100644 --- a/src/verify/function_friendship.sql +++ b/src/verify/function_friendship.sql @@ -15,6 +15,10 @@ BEGIN RAISE EXCEPTION 'Test failed: vibetype_account does not have EXECUTE privilege for vibetype.friendship_notify_request(UUID, TEXT).'; END IF; + IF NOT (SELECT pg_catalog.has_function_privilege('vibetype_account', 'vibetype.friendship_reject(UUID)', 'EXECUTE')) THEN + RAISE EXCEPTION 'Test failed: vibetype_account does not have EXECUTE privilege for vibetype.friendship_reject(UUID).'; + END IF; + IF NOT (SELECT pg_catalog.has_function_privilege('vibetype_account', 'vibetype.friendship_request(UUID, TEXT)', 'EXECUTE')) THEN RAISE EXCEPTION 'Test failed: vibetype_account does not have EXECUTE privilege for vibetype.friendship_request(UUID, TEXT).'; END IF; diff --git a/src/verify/table_friendship.sql b/src/verify/table_friendship.sql index b57e3a90..ff53670a 100644 --- a/src/verify/table_friendship.sql +++ b/src/verify/table_friendship.sql @@ -1,11 +1,19 @@ BEGIN; +SELECT + id, + account_id, + friend_account_id, + created_at, + created_by +FROM vibetype.friendship_request +WHERE FALSE; + SELECT id, account_id, friend_account_id, is_close_friend, - status, created_at, created_by, updated_at, @@ -34,6 +42,20 @@ BEGIN ASSERT NOT (SELECT pg_catalog.has_table_privilege(current_setting('role.vibetype_username'), 'vibetype.friendship', 'INSERT')); ASSERT NOT (SELECT pg_catalog.has_table_privilege(current_setting('role.vibetype_username'), 'vibetype.friendship', 'UPDATE')); ASSERT NOT (SELECT pg_catalog.has_table_privilege(current_setting('role.vibetype_username'), 'vibetype.friendship', 'DELETE')); + + ASSERT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.friendship_request', 'SELECT')); + ASSERT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.friendship_request', 'INSERT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.friendship_request', 'UPDATE')); + ASSERT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.friendship_request', 'DELETE')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_anonymous', 'vibetype.friendship_request', 'SELECT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_anonymous', 'vibetype.friendship_request', 'INSERT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_anonymous', 'vibetype.friendship_request', 'UPDATE')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_anonymous', 'vibetype.friendship_request', 'DELETE')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege(current_setting('role.vibetype_username'), 'vibetype.friendship_request', 'SELECT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege(current_setting('role.vibetype_username'), 'vibetype.friendship_request', 'INSERT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege(current_setting('role.vibetype_username'), 'vibetype.friendship_request', 'UPDATE')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege(current_setting('role.vibetype_username'), 'vibetype.friendship_request', 'DELETE')); + END $$; ROLLBACK; diff --git a/test/fixture/schema_vibetype.definition.sql b/test/fixture/schema_vibetype.definition.sql index 7430cf3a..404037e2 100644 --- a/test/fixture/schema_vibetype.definition.sql +++ b/test/fixture/schema_vibetype.definition.sql @@ -136,26 +136,6 @@ ALTER TYPE vibetype.event_visibility OWNER TO ci; COMMENT ON TYPE vibetype.event_visibility IS 'Possible visibilities of events and event groups: public, private and unlisted.'; --- --- Name: friendship_status; Type: TYPE; Schema: vibetype; Owner: ci --- - -CREATE TYPE vibetype.friendship_status AS ENUM ( - 'accepted', - 'requested' -); - - -ALTER TYPE vibetype.friendship_status OWNER TO ci; - --- --- Name: TYPE friendship_status; Type: COMMENT; Schema: vibetype; Owner: ci --- - -COMMENT ON TYPE vibetype.friendship_status IS 'Possible status values of a friend relation. -There is no status `rejected` because friendship records will be deleted when a friendship request is rejected.'; - - -- -- Name: invitation_feedback; Type: TYPE; Schema: vibetype; Owner: ci -- @@ -1319,24 +1299,27 @@ CREATE FUNCTION vibetype.friendship_accept(requestor_account_id uuid) RETURNS vo AS $$ DECLARE _friend_account_id UUID; - _count INTEGER; + _id UUID; BEGIN _friend_account_id := vibetype.invoker_account_id(); - UPDATE vibetype.friendship SET - status = 'accepted'::vibetype.friendship_status - -- updated_by filled by trigger - WHERE account_id = requestor_account_id AND friend_account_id = _friend_account_id - AND status = 'requested'::vibetype.friendship_status; + SELECT id INTO _id + FROM vibetype.friendship_request + WHERE account_id = requestor_account_id AND friend_account_id = _friend_account_id; - GET DIAGNOSTICS _count = ROW_COUNT; - IF _count = 0 THEN + IF _id IS NULL THEN RAISE EXCEPTION 'Friendship request does not exist' USING ERRCODE = 'VTFAC'; END IF; - INSERT INTO vibetype.friendship(account_id, friend_account_id, status, created_by) - VALUES (_friend_account_id, requestor_account_id, 'accepted'::vibetype.friendship_status, _friend_account_id); + INSERT INTO vibetype.friendship(account_id, friend_account_id, created_by) + VALUES (requestor_account_id, _friend_account_id, requestor_account_id); + + INSERT INTO vibetype.friendship(account_id, friend_account_id, created_by) + VALUES (_friend_account_id, requestor_account_id, _friend_account_id); + + DELETE FROM vibetype.friendship_request + WHERE account_id = requestor_account_id AND friend_account_id = vibetype.invoker_account_id(); END; $$; @@ -1347,7 +1330,10 @@ ALTER FUNCTION vibetype.friendship_accept(requestor_account_id uuid) OWNER TO ci -- Name: FUNCTION friendship_accept(requestor_account_id uuid); Type: COMMENT; Schema: vibetype; Owner: ci -- -COMMENT ON FUNCTION vibetype.friendship_accept(requestor_account_id uuid) IS 'Accepts a friendship request.\n\nError codes:\n- **VTFAC** when a corresponding friendship request does not exist.'; +COMMENT ON FUNCTION vibetype.friendship_accept(requestor_account_id uuid) IS 'Accepts a friendship request. + +Error codes: +- **VTFAC** when a corresponding friendship request does not exist.'; -- @@ -1376,7 +1362,7 @@ ALTER FUNCTION vibetype.friendship_cancel(friend_account_id uuid) OWNER TO ci; -- Name: FUNCTION friendship_cancel(friend_account_id uuid); Type: COMMENT; Schema: vibetype; Owner: ci -- -COMMENT ON FUNCTION vibetype.friendship_cancel(friend_account_id uuid) IS 'Rejects or cancels a friendship (in both directions).'; +COMMENT ON FUNCTION vibetype.friendship_cancel(friend_account_id uuid) IS 'Cancels a friendship (in both directions) if it exists.'; -- @@ -1412,6 +1398,30 @@ ALTER FUNCTION vibetype.friendship_notify_request(friend_account_id uuid, langua COMMENT ON FUNCTION vibetype.friendship_notify_request(friend_account_id uuid, language text) IS 'Creates a notification for a friendship_request'; +-- +-- Name: friendship_reject(uuid); Type: FUNCTION; Schema: vibetype; Owner: ci +-- + +CREATE FUNCTION vibetype.friendship_reject(requestor_account_id uuid) RETURNS void + LANGUAGE plpgsql SECURITY DEFINER + AS $$ +BEGIN + + DELETE FROM vibetype.friendship_request + WHERE account_id = requestor_account_id AND friend_account_id = vibetype.invoker_account_id(); + +END; $$; + + +ALTER FUNCTION vibetype.friendship_reject(requestor_account_id uuid) OWNER TO ci; + +-- +-- Name: FUNCTION friendship_reject(requestor_account_id uuid); Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON FUNCTION vibetype.friendship_reject(requestor_account_id uuid) IS 'Rejects a friendship request'; + + -- -- Name: friendship_request(uuid, text); Type: FUNCTION; Schema: vibetype; Owner: ci -- @@ -1429,14 +1439,23 @@ BEGIN SELECT 1 FROM vibetype.friendship f WHERE (f.account_id = _account_id AND f.friend_account_id = friendship_request.friend_account_id) - OR (f.account_id = friendship_request.friend_account_id AND f.friend_account_id = _account_id) ) THEN - RAISE EXCEPTION 'Friendship already exists or has already been requested.' USING ERRCODE = 'VTREQ'; + RAISE EXCEPTION 'Friendship already exists.' USING ERRCODE = 'VTFEX'; + END IF; + + IF EXISTS( + SELECT 1 + FROM vibetype.friendship_request r + WHERE (r.account_id = _account_id AND r.friend_account_id = friendship_request.friend_account_id) + OR (r.account_id = friendship_request.friend_account_id AND r.friend_account_id = _account_id) + ) + THEN + RAISE EXCEPTION 'There is already a friendship request.' USING ERRCODE = 'VTREQ'; END IF; - INSERT INTO vibetype.friendship(account_id, friend_account_id, status, created_by) - VALUES (_account_id, friendship_request.friend_account_id, 'requested'::vibetype.friendship_status, _account_id); + INSERT INTO vibetype.friendship_request(account_id, friend_account_id, created_by) + VALUES (_account_id, friendship_request.friend_account_id, _account_id); PERFORM vibetype.friendship_notify_request(friendship_request.friend_account_id, friendship_request.language); @@ -1449,7 +1468,11 @@ ALTER FUNCTION vibetype.friendship_request(friend_account_id uuid, language text -- Name: FUNCTION friendship_request(friend_account_id uuid, language text); Type: COMMENT; Schema: vibetype; Owner: ci -- -COMMENT ON FUNCTION vibetype.friendship_request(friend_account_id uuid, language text) IS 'Starts a new friendship request.\n\nError codes:\n- **VTREQ** when the friendship already exists or has already been requested.'; +COMMENT ON FUNCTION vibetype.friendship_request(friend_account_id uuid, language text) IS 'Starts a new friendship request. + +Error codes: +- **VTFEX** when the friendship already exists. +- **VTREQ** when there is already a friendship request.'; -- @@ -1461,17 +1484,19 @@ CREATE FUNCTION vibetype.friendship_toggle_closeness(friend_account_id uuid) RET AS $$ DECLARE _account_id UUID; + _id UUID; _is_close_friend BOOLEAN; - current_status vibetype.friendship_status; BEGIN _account_id := vibetype.invoker_account_id(); - SELECT status INTO current_status + SELECT f.id + INTO _id FROM vibetype.friendship f - WHERE f.account_id = _account_id AND f.friend_account_id = friendship_toggle_closeness.friend_account_id; + WHERE f.account_id = _account_id + AND f.friend_account_id = friendship_toggle_closeness.friend_account_id; - IF current_status IS NULL OR current_status != 'accepted'::vibetype.friendship_status THEN + IF _id IS NULL THEN RAISE EXCEPTION 'Friendship does not exist' USING ERRCODE = 'VTFTC'; END IF; @@ -1492,7 +1517,10 @@ ALTER FUNCTION vibetype.friendship_toggle_closeness(friend_account_id uuid) OWNE -- Name: FUNCTION friendship_toggle_closeness(friend_account_id uuid); Type: COMMENT; Schema: vibetype; Owner: ci -- -COMMENT ON FUNCTION vibetype.friendship_toggle_closeness(friend_account_id uuid) IS 'Toggles a frien1dship relation between ''not a close friend'' and ''close friend''.\n\nError codes:\n- **VTFTC** when the friendship does not exist.'; +COMMENT ON FUNCTION vibetype.friendship_toggle_closeness(friend_account_id uuid) IS 'Toggles a friendship relation between ''not a close friend'' and ''close friend''. + +Error codes: +- **VTFTC** when the friendship does not exist.'; -- @@ -3501,7 +3529,6 @@ CREATE TABLE vibetype.friendship ( account_id uuid NOT NULL, friend_account_id uuid NOT NULL, is_close_friend boolean DEFAULT false NOT NULL, - status vibetype.friendship_status DEFAULT 'requested'::vibetype.friendship_status NOT NULL, created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, created_by uuid NOT NULL, updated_at timestamp with time zone, @@ -3552,14 +3579,6 @@ COMMENT ON COLUMN vibetype.friendship.is_close_friend IS '@omit create The flag indicating whether account_id considers friend_account_id as a close friend or not.'; --- --- Name: COLUMN friendship.status; Type: COMMENT; Schema: vibetype; Owner: ci --- - -COMMENT ON COLUMN vibetype.friendship.status IS '@omit create -The status of the friend relation.'; - - -- -- Name: COLUMN friendship.created_at; Type: COMMENT; Schema: vibetype; Owner: ci -- @@ -3592,6 +3611,23 @@ COMMENT ON COLUMN vibetype.friendship.updated_by IS '@omit create,update The account that updated the friend relation''s status.'; +-- +-- Name: friendship_request; Type: TABLE; Schema: vibetype; Owner: ci +-- + +CREATE TABLE vibetype.friendship_request ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + account_id uuid NOT NULL, + friend_account_id uuid NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + created_by uuid NOT NULL, + CONSTRAINT friendship_creator_friend CHECK ((account_id <> friend_account_id)), + CONSTRAINT friendship_creator_participant CHECK ((created_by = account_id)) +); + + +ALTER TABLE vibetype.friendship_request OWNER TO ci; + -- -- Name: guest_flat; Type: VIEW; Schema: vibetype; Owner: ci -- @@ -4811,6 +4847,22 @@ ALTER TABLE ONLY vibetype.friendship ADD CONSTRAINT friendship_pkey PRIMARY KEY (id); +-- +-- Name: friendship_request friendship_request_account_id_friend_account_id_key; Type: CONSTRAINT; Schema: vibetype; Owner: ci +-- + +ALTER TABLE ONLY vibetype.friendship_request + ADD CONSTRAINT friendship_request_account_id_friend_account_id_key UNIQUE (account_id, friend_account_id); + + +-- +-- Name: friendship_request friendship_request_pkey; Type: CONSTRAINT; Schema: vibetype; Owner: ci +-- + +ALTER TABLE ONLY vibetype.friendship_request + ADD CONSTRAINT friendship_request_pkey PRIMARY KEY (id); + + -- -- Name: guest guest_event_id_contact_id_key; Type: CONSTRAINT; Schema: vibetype; Owner: ci -- @@ -5496,6 +5548,30 @@ ALTER TABLE ONLY vibetype.friendship ADD CONSTRAINT friendship_friend_account_id_fkey FOREIGN KEY (friend_account_id) REFERENCES vibetype.account(id) ON DELETE CASCADE; +-- +-- Name: friendship_request friendship_request_account_id_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci +-- + +ALTER TABLE ONLY vibetype.friendship_request + ADD CONSTRAINT friendship_request_account_id_fkey FOREIGN KEY (account_id) REFERENCES vibetype.account(id) ON DELETE CASCADE; + + +-- +-- Name: friendship_request friendship_request_created_by_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci +-- + +ALTER TABLE ONLY vibetype.friendship_request + ADD CONSTRAINT friendship_request_created_by_fkey FOREIGN KEY (created_by) REFERENCES vibetype.account(id) ON DELETE CASCADE; + + +-- +-- Name: friendship_request friendship_request_friend_account_id_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci +-- + +ALTER TABLE ONLY vibetype.friendship_request + ADD CONSTRAINT friendship_request_friend_account_id_fkey FOREIGN KEY (friend_account_id) REFERENCES vibetype.account(id) ON DELETE CASCADE; + + -- -- Name: friendship friendship_updated_by_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci -- @@ -5945,44 +6021,81 @@ ALTER TABLE vibetype.friendship ENABLE ROW LEVEL SECURITY; -- Name: friendship friendship_delete; Type: POLICY; Schema: vibetype; Owner: ci -- -CREATE POLICY friendship_delete ON vibetype.friendship FOR DELETE USING (true); +CREATE POLICY friendship_delete ON vibetype.friendship FOR DELETE USING (((account_id = vibetype.invoker_account_id()) OR (friend_account_id = vibetype.invoker_account_id()))); -- -- Name: friendship friendship_insert; Type: POLICY; Schema: vibetype; Owner: ci -- -CREATE POLICY friendship_insert ON vibetype.friendship FOR INSERT WITH CHECK ((created_by = vibetype.invoker_account_id())); +CREATE POLICY friendship_insert ON vibetype.friendship FOR INSERT WITH CHECK ((((account_id, friend_account_id, created_by) IN ( SELECT friendship_request.account_id, + friendship_request.friend_account_id, + friendship_request.account_id + FROM vibetype.friendship_request + WHERE (friendship_request.friend_account_id = vibetype.invoker_account_id()))) OR ((account_id, friend_account_id, created_by) IN ( SELECT friendship_request.friend_account_id, + friendship_request.account_id, + friendship_request.friend_account_id + FROM vibetype.friendship_request + WHERE (friendship_request.friend_account_id = vibetype.invoker_account_id()))))); -- -- Name: friendship friendship_not_blocked; Type: POLICY; Schema: vibetype; Owner: ci -- -CREATE POLICY friendship_not_blocked ON vibetype.friendship USING (((NOT (account_id IN ( SELECT account_block_ids.id +CREATE POLICY friendship_not_blocked ON vibetype.friendship AS RESTRICTIVE USING (((NOT (account_id IN ( SELECT account_block_ids.id FROM vibetype_private.account_block_ids() account_block_ids(id)))) AND (NOT (friend_account_id IN ( SELECT account_block_ids.id FROM vibetype_private.account_block_ids() account_block_ids(id)))))); -- --- Name: friendship friendship_select; Type: POLICY; Schema: vibetype; Owner: ci +-- Name: friendship_request; Type: ROW SECURITY; Schema: vibetype; Owner: ci -- -CREATE POLICY friendship_select ON vibetype.friendship FOR SELECT USING (((account_id = vibetype.invoker_account_id()) OR (friend_account_id = vibetype.invoker_account_id()))); +ALTER TABLE vibetype.friendship_request ENABLE ROW LEVEL SECURITY; +-- +-- Name: friendship_request friendship_request_delete; Type: POLICY; Schema: vibetype; Owner: ci +-- +CREATE POLICY friendship_request_delete ON vibetype.friendship_request FOR DELETE USING ((friend_account_id = vibetype.invoker_account_id())); + + +-- +-- Name: friendship_request friendship_request_insert; Type: POLICY; Schema: vibetype; Owner: ci +-- + +CREATE POLICY friendship_request_insert ON vibetype.friendship_request FOR INSERT WITH CHECK ((created_by = vibetype.invoker_account_id())); + + +-- +-- Name: friendship_request friendship_request_not_blocked; Type: POLICY; Schema: vibetype; Owner: ci +-- + +CREATE POLICY friendship_request_not_blocked ON vibetype.friendship_request USING (((NOT (account_id IN ( SELECT account_block_ids.id + FROM vibetype_private.account_block_ids() account_block_ids(id)))) AND (NOT (friend_account_id IN ( SELECT account_block_ids.id + FROM vibetype_private.account_block_ids() account_block_ids(id)))))); + + +-- +-- Name: friendship_request friendship_request_select; Type: POLICY; Schema: vibetype; Owner: ci -- --- Name: friendship friendship_update_accept; Type: POLICY; Schema: vibetype; Owner: ci + +CREATE POLICY friendship_request_select ON vibetype.friendship_request FOR SELECT USING (((account_id = vibetype.invoker_account_id()) OR (friend_account_id = vibetype.invoker_account_id()))); + + +-- +-- Name: friendship friendship_select; Type: POLICY; Schema: vibetype; Owner: ci -- -CREATE POLICY friendship_update_accept ON vibetype.friendship FOR UPDATE USING (((friend_account_id = vibetype.invoker_account_id()) AND (status = 'requested'::vibetype.friendship_status))) WITH CHECK (((status = 'accepted'::vibetype.friendship_status) AND (updated_by = vibetype.invoker_account_id()))); +CREATE POLICY friendship_select ON vibetype.friendship FOR SELECT USING ((account_id = vibetype.invoker_account_id())); -- --- Name: friendship friendship_update_toggle_closeness; Type: POLICY; Schema: vibetype; Owner: ci +-- Name: friendship friendship_update; Type: POLICY; Schema: vibetype; Owner: ci -- -CREATE POLICY friendship_update_toggle_closeness ON vibetype.friendship FOR UPDATE USING (((status = 'accepted'::vibetype.friendship_status) AND (account_id = vibetype.invoker_account_id()))) WITH CHECK (((status = 'accepted'::vibetype.friendship_status) AND (updated_by = vibetype.invoker_account_id()))); +CREATE POLICY friendship_update ON vibetype.friendship FOR UPDATE USING ((account_id = vibetype.invoker_account_id())) WITH CHECK ((updated_by = vibetype.invoker_account_id())); -- @@ -6434,6 +6547,14 @@ REVOKE ALL ON FUNCTION vibetype.friendship_notify_request(friend_account_id uuid GRANT ALL ON FUNCTION vibetype.friendship_notify_request(friend_account_id uuid, language text) TO vibetype_account; +-- +-- Name: FUNCTION friendship_reject(requestor_account_id uuid); Type: ACL; Schema: vibetype; Owner: ci +-- + +REVOKE ALL ON FUNCTION vibetype.friendship_reject(requestor_account_id uuid) FROM PUBLIC; +GRANT ALL ON FUNCTION vibetype.friendship_reject(requestor_account_id uuid) TO vibetype_account; + + -- -- Name: FUNCTION friendship_request(friend_account_id uuid, language text); Type: ACL; Schema: vibetype; Owner: ci -- @@ -6807,6 +6928,13 @@ GRANT SELECT,INSERT,DELETE ON TABLE vibetype.event_upload TO vibetype_account; GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE vibetype.friendship TO vibetype_account; +-- +-- Name: TABLE friendship_request; Type: ACL; Schema: vibetype; Owner: ci +-- + +GRANT SELECT,INSERT,DELETE ON TABLE vibetype.friendship_request TO vibetype_account; + + -- -- Name: TABLE guest_flat; Type: ACL; Schema: vibetype; Owner: ci -- diff --git a/test/logic/scenario/model/friendship.sql b/test/logic/scenario/model/friendship.sql index 4ced4c97..6008dc1d 100644 --- a/test/logic/scenario/model/friendship.sql +++ b/test/logic/scenario/model/friendship.sql @@ -1,4 +1,4 @@ -\echo test_friendship.. +\echo test_friendship... BEGIN; @@ -8,51 +8,44 @@ DECLARE accountB UUID; accountC UUID; rec RECORD; + _is_close_friend BOOLEAN; + _invoker_account_id UUID; BEGIN - -- before all + -- create accounts accountA := vibetype_test.account_registration_verified('username-a', 'email+a@example.com'); accountB := vibetype_test.account_registration_verified('username-b', 'email+b@example.com'); accountC := vibetype_test.account_registration_verified('username-c', 'email+c@example.com'); + PERFORM vibetype_test.friendship_request_test('before A sends request to B', accountA, accountB, false); + -- friendship request from user A to B PERFORM vibetype_test.friendship_request(accountA, accountB, 'de'); - - RAISE NOTICE '----'; - FOR rec IN - SELECT a.username, b.username as friend_username, f.is_close_friend, f.status - FROM vibetype.friendship f - JOIN vibetype.account a ON f.account_id = a.id - JOIN vibetype.account b ON f.friend_account_id = b.id - LOOP - RAISE NOTICE 'friendship: account = %, friend_account = %, is_close_friend = %, status = %', rec.username, rec.friend_username, rec.is_close_friend, rec.status; - END LOOP; - - PERFORM vibetype_test.friendship_test('A sends B a friendship request (1)', accountA, accountB, false, 'requested', 1); - PERFORM vibetype_test.friendship_test('A sends B a friendship request (2)', accountB, accountA, false, 'requested', 0); - PERFORM vibetype_test.friendship_test('A sends B a friendship request (3)', accountA, accountB, false, null, 1); - PERFORM vibetype_test.friendship_test('A sends B a friendship request (3)', accountB, accountA, false, null, 0); + PERFORM vibetype_test.friendship_request_test('after A sends request to B (1)', accountA, accountB, true); + PERFORM vibetype_test.friendship_test('after A sends request to B (2)', accountA, accountB, null, 0); + PERFORM vibetype_test.friendship_test('after A sends request to B (3)', accountB, accountA, null, 0); -- B accepts A's friendship request PERFORM vibetype_test.friendship_accept(accountB, accountA); - PERFORM vibetype_test.friendship_test('B accepts friendship request from A (1)', accountA, accountB, false, 'requested', 0); - PERFORM vibetype_test.friendship_test('B accepts friendship request from A (2)', accountA, accountB, false, 'accepted', 1); - PERFORM vibetype_test.friendship_test('B accepts friendship request from A (3)', accountB, accountA, false, 'accepted', 1); + PERFORM vibetype_test.friendship_request_test('B accepts friendship request from A (1)', accountA, accountB, false); + PERFORM vibetype_test.friendship_test('B accepts friendship request from A (2)', accountA, accountB, false, 1); + PERFORM vibetype_test.friendship_test('B accepts friendship request from A (3)', accountB, accountA, false, 1); -- friendship request from user C to A PERFORM vibetype_test.friendship_request(accountC, accountA, 'de'); - PERFORM vibetype_test.friendship_test('There is still only one accepted friendship for user A', accountA, null, false, 'accepted', 1); - PERFORM vibetype_test.friendship_test('There is a new requested friendship for user C (1)', accountC, null, false, 'requested', 1); - PERFORM vibetype_test.friendship_test('There is a new requested friendship for user C (2)', accountA, accountC, false, null, 0); - PERFORM vibetype_test.friendship_test('User B is still a friend of user A (1)', accountA, accountB, false, null, 1); - PERFORM vibetype_test.friendship_test('User B is still a friend of user A (2)', accountB, accountA, false, null, 1); - PERFORM vibetype_test.friendship_test('User C has no friends', accountC, null, false, 'accepted', 0); + PERFORM vibetype_test.friendship_request_test('after C sends request to A (1)', accountC, accountA, true); + PERFORM vibetype_test.friendship_test('after C sends request to A (2)', accountC, accountA, null, 0); + PERFORM vibetype_test.friendship_test('after A sends request to B (3)', accountA, accountC, null, 0); + PERFORM vibetype_test.friendship_test('User B is still a friend of user A (1)', accountA, accountB, null, 1); + PERFORM vibetype_test.friendship_test('User A is still a friend of user B (2)', accountB, accountA, null, 1); + PERFORM vibetype_test.friendship_test('User C has no friends', accountC, null, null, 0); BEGIN + -- C sends another request to A, should lead to exception VTREQ PERFORM vibetype_test.friendship_request(accountA, accountC, 'de'); - RAISE 'It was possible to request a friendship more than once.'; + RAISE 'C sends another request to A: it was possible to request a friendship more than once.'; EXCEPTION WHEN OTHERS THEN IF SQLSTATE != 'VTREQ' THEN @@ -60,35 +53,94 @@ BEGIN END IF; END; - -- friendship rejection - PERFORM vibetype_test.friendship_cancel(accountA, accountC); - PERFORM vibetype_test.friendship_test('After user A rejected user C''s friendship request (1)', accountC, accountA, false, null, 0); - PERFORM vibetype_test.friendship_test('After user A rejected user C''s friendship request (2)', accountA, accountC, false, null, 0); - - -- a new friendship request from user C to A, this time accepted by A - PERFORM vibetype_test.friendship_request(accountC, accountA, 'de'); - PERFORM vibetype_test.friendship_accept(accountA, accountC); - PERFORM vibetype_test.friendship_test('Count the number of A''s friends', accountA, null, false, 'accepted', 2); - PERFORM vibetype_test.friendship_test('C is a friend of A (1)', accountA, accountC, false, 'accepted', 1); - PERFORM vibetype_test.friendship_test('C is a friend of A (2)', accountC, accountA, false, 'accepted', 1); + BEGIN + -- A sends a request to C, should lead to exception VTREQ + PERFORM vibetype_test.friendship_request(accountA, accountC, 'de'); + RAISE 'A sends a request to C: it was possible to request a friendship more than once.'; + EXCEPTION + WHEN OTHERS THEN + IF SQLSTATE != 'VTREQ' THEN + RAISE; + END IF; + END; - -- friendship request from user B to A - PERFORM vibetype_test.friendship_request(accountB, accountC, 'de'); + BEGIN + -- A sends a new request to B, should lead to exception VTFEX + PERFORM vibetype_test.friendship_request(accountA, accountB, 'de'); + RAISE 'A sends a new request to B: it was possible to request for an already existing friendship.'; + EXCEPTION + WHEN OTHERS THEN + IF SQLSTATE != 'VTFEX' THEN + RAISE; + END IF; + END; BEGIN - PERFORM vibetype_test.friendship_toggle_closeness(accountB, accountC); - RAISE 'It was possible to toggle closeness in a friendship request.'; + -- B sends a new request to A, should lead to exception VTFEX + PERFORM vibetype_test.friendship_request(accountB, accountA, 'de'); + RAISE 'It was possible to request for an already existing friendship.'; EXCEPTION WHEN OTHERS THEN - IF SQLSTATE != 'VTFTC' THEN + IF SQLSTATE != 'VTFEX' THEN RAISE; END IF; END; + -- A rejects friendship request from C + PERFORM vibetype_test.friendship_reject(accountA, accountC); + PERFORM vibetype_test.friendship_test('After A rejected C''s friendship request (1)', accountC, accountA, null, 0); + PERFORM vibetype_test.friendship_test('After A rejected C''s friendship request (2)', accountA, accountC, null, 0); + + -- a new friendship request from user C to A, this time accepted by A + PERFORM vibetype_test.friendship_request(accountC, accountA, 'de'); + PERFORM vibetype_test.friendship_accept(accountA, accountC); + PERFORM vibetype_test.friendship_test('Count the number of A''s friends', accountA, null, null, 2); + PERFORM vibetype_test.friendship_test('C is a friend of A (1)', accountA, accountC, null, 1); + PERFORM vibetype_test.friendship_test('C is a friend of A (2)', accountC, accountA, null, 1); + + -- friendship request from user B to A + PERFORM vibetype_test.friendship_request(accountB, accountC, 'de'); + + -- B marks A as a close friend PERFORM vibetype_test.friendship_toggle_closeness(accountB, accountA); - PERFORM vibetype_test.friendship_test('B marks A as a close friend (1)', accountB, accountA, true, 'accepted', 1); - PERFORM vibetype_test.friendship_test('B marks A as a close friend (2)', accountA, accountB, true, NULL, 0); + +/* + RAISE NOTICE '----'; + FOR rec IN + SELECT a.username, b.username as friend_username, f.is_close_friend + FROM vibetype.friendship f + JOIN vibetype.account a ON f.account_id = a.id + JOIN vibetype.account b ON f.friend_account_id = b.id + LOOP + RAISE NOTICE 'friendship: account = %, friend_account = %, is_close_friend = %', rec.username, rec.friend_username, rec.is_close_friend; + END LOOP; +*/ + + PERFORM vibetype_test.friendship_test('B marks A as a close friend (1)', accountB, accountA, true, 1); + PERFORM vibetype_test.friendship_test('B marks A as a close friend (2)', accountA, accountB, false, 1); + + -- B unmarks A as a close friend + PERFORM vibetype_test.friendship_toggle_closeness(accountB, accountA); + PERFORM vibetype_test.friendship_test('B marks A as a close friend (1)', accountB, accountA, false, 1); + + -- C marks A as a close friend + PERFORM vibetype_test.friendship_toggle_closeness(accountC, accountA); + + -- A wants to find out, if A is a close friend of C. The result should be NULL. + + SET LOCAL role = 'vibetype_account'; + EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || accountA || ''''; + + SELECT is_close_friend INTO _is_close_friend + FROM vibetype.friendship + WHERE account_id = accountC and friend_account_id = accountA; + + IF _is_close_friend IS NOT NULL THEN + RAISE EXCEPTION 'Closeness should not be disclosed to A.'; + END IF; + + SET LOCAL ROLE NONE; END $$; diff --git a/test/logic/utility/model/friendship.sql b/test/logic/utility/model/friendship.sql index c406ae6d..56962275 100644 --- a/test/logic/utility/model/friendship.sql +++ b/test/logic/utility/model/friendship.sql @@ -33,6 +33,22 @@ END $$ LANGUAGE plpgsql; GRANT EXECUTE ON FUNCTION vibetype_test.friendship_cancel(UUID, UUID) TO vibetype_account; +CREATE OR REPLACE FUNCTION vibetype_test.friendship_reject ( + _invoker_account_id UUID, + _friend_account_id UUID +) RETURNS VOID AS $$ +BEGIN + SET LOCAL role = 'vibetype_account'; + EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || _invoker_account_id || ''''; + + PERFORM vibetype.friendship_reject(_friend_account_id); + + SET LOCAL ROLE NONE; +END $$ LANGUAGE plpgsql; + +GRANT EXECUTE ON FUNCTION vibetype_test.friendship_reject(UUID, UUID) TO vibetype_account; + + CREATE OR REPLACE FUNCTION vibetype_test.friendship_request ( _invoker_account_id UUID, _friend_account_id UUID, @@ -68,9 +84,8 @@ GRANT EXECUTE ON FUNCTION vibetype_test.friendship_toggle_closeness(UUID, UUID) CREATE OR REPLACE FUNCTION vibetype_test.friendship_test ( _test_case TEXT, _invoker_account_id UUID, - _friend_account_id UUID, + _friend_account_id UUID, -- _friend_account_id IS NULL means "any friend" _is_close_friend BOOLEAN, -- _is_close_friend IS NULL means "any boolean value" - _status TEXT, -- _status IS NULL means "any status" _expected_count INTEGER ) RETURNS VOID AS $$ DECLARE @@ -82,15 +97,45 @@ BEGIN SELECT count(*) INTO _result FROM vibetype.friendship WHERE account_id = _invoker_account_id - AND (_status IS NULL OR status = _status::vibetype.friendship_status) - AND (_is_close_friend IS NULL OR is_close_friend = _is_close_friend) - AND (_friend_account_id IS NULL OR friend_account_id = _friend_account_id); + AND (_friend_account_id IS NULL OR friend_account_id = _friend_account_id) + AND (_is_close_friend IS NULL OR is_close_friend = _is_close_friend); IF _result != _expected_count THEN - RAISE EXCEPTION 'Expected count was % but result is %.', _expected_count, _result USING ERRCODE = 'VTTST'; + RAISE EXCEPTION '%: expected count was % but result is %.', _test_case, _expected_count, _result USING ERRCODE = 'VTTST'; + END IF; + + SET LOCAL ROLE NONE; +END $$ LANGUAGE plpgsql; + +GRANT EXECUTE ON FUNCTION vibetype_test.friendship_test(TEXT, UUID, UUID, BOOLEAN, INTEGER) TO vibetype_account; + + +CREATE OR REPLACE FUNCTION vibetype_test.friendship_request_test ( + _test_case TEXT, + _invoker_account_id UUID, + _friend_account_id UUID, + _expected_to_exist BOOLEAN +) RETURNS VOID AS $$ +DECLARE + _id UUID; +BEGIN + SET LOCAL role = 'vibetype_account'; + EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || _invoker_account_id || ''''; + + SELECT id INTO _id + FROM vibetype.friendship_request + WHERE account_id = _invoker_account_id + AND friend_account_id = _friend_account_id; + + IF _id IS NULL AND _expected_to_exist THEN + RAISE EXCEPTION '%: friendship request expected to exist but not present.', _test_case USING ERRCODE = 'VTFRT'; + END IF; + + IF _id IS NOT NULL AND NOT _expected_to_exist THEN + RAISE EXCEPTION '%: friendship request exists but is not expected to exist.', _test_case USING ERRCODE = 'VTFRT'; END IF; SET LOCAL ROLE NONE; END $$ LANGUAGE plpgsql; -GRANT EXECUTE ON FUNCTION vibetype_test.friendship_test(TEXT, UUID, UUID, BOOLEAN, TEXT, INTEGER) TO vibetype_account; +GRANT EXECUTE ON FUNCTION vibetype_test.friendship_request_test(TEXT, UUID, UUID, BOOLEAN) TO vibetype_account; From 809d3f3e7e083394735384ea4a093369bd96146b Mon Sep 17 00:00:00 2001 From: Sven Thelemann Date: Tue, 12 Aug 2025 19:31:14 +0200 Subject: [PATCH 3/8] feat(friendship): change visibility of friendship relationships If an account A marks account B as a close friend, B can now see this information. An account C cannot select any friendship in which C is not involved. --- src/deploy/table_friendship.sql | 4 +- test/fixture/schema_vibetype.definition.sql | 4 +- test/logic/scenario/model/friendship.sql | 85 ++++++++++++--------- test/logic/utility/model/friendship.sql | 10 ++- 4 files changed, 58 insertions(+), 45 deletions(-) diff --git a/src/deploy/table_friendship.sql b/src/deploy/table_friendship.sql index eb6f47f4..73648a13 100644 --- a/src/deploy/table_friendship.sql +++ b/src/deploy/table_friendship.sql @@ -18,7 +18,7 @@ GRANT SELECT, INSERT, DELETE ON TABLE vibetype.friendship_request TO vibetype_ac ALTER TABLE vibetype.friendship_request ENABLE ROW LEVEL SECURITY; -CREATE POLICY friendship_request_not_blocked ON vibetype.friendship_request FOR ALL +CREATE POLICY friendship_request_not_blocked ON vibetype.friendship_request AS RESTRICTIVE FOR ALL USING ( account_id NOT IN (SELECT id FROM vibetype_private.account_block_ids()) AND friend_account_id NOT IN (SELECT id FROM vibetype_private.account_block_ids()) @@ -101,6 +101,8 @@ USING ( CREATE POLICY friendship_select ON vibetype.friendship FOR SELECT USING ( account_id = vibetype.invoker_account_id() + OR + friend_account_id = vibetype.invoker_account_id() ); -- Only allow creation by the current user and only if a friendship request is present. diff --git a/test/fixture/schema_vibetype.definition.sql b/test/fixture/schema_vibetype.definition.sql index 404037e2..7912fc0b 100644 --- a/test/fixture/schema_vibetype.definition.sql +++ b/test/fixture/schema_vibetype.definition.sql @@ -6072,7 +6072,7 @@ CREATE POLICY friendship_request_insert ON vibetype.friendship_request FOR INSER -- Name: friendship_request friendship_request_not_blocked; Type: POLICY; Schema: vibetype; Owner: ci -- -CREATE POLICY friendship_request_not_blocked ON vibetype.friendship_request USING (((NOT (account_id IN ( SELECT account_block_ids.id +CREATE POLICY friendship_request_not_blocked ON vibetype.friendship_request AS RESTRICTIVE USING (((NOT (account_id IN ( SELECT account_block_ids.id FROM vibetype_private.account_block_ids() account_block_ids(id)))) AND (NOT (friend_account_id IN ( SELECT account_block_ids.id FROM vibetype_private.account_block_ids() account_block_ids(id)))))); @@ -6088,7 +6088,7 @@ CREATE POLICY friendship_request_select ON vibetype.friendship_request FOR SELEC -- Name: friendship friendship_select; Type: POLICY; Schema: vibetype; Owner: ci -- -CREATE POLICY friendship_select ON vibetype.friendship FOR SELECT USING ((account_id = vibetype.invoker_account_id())); +CREATE POLICY friendship_select ON vibetype.friendship FOR SELECT USING (((account_id = vibetype.invoker_account_id()) OR (friend_account_id = vibetype.invoker_account_id()))); -- diff --git a/test/logic/scenario/model/friendship.sql b/test/logic/scenario/model/friendship.sql index 6008dc1d..91536e16 100644 --- a/test/logic/scenario/model/friendship.sql +++ b/test/logic/scenario/model/friendship.sql @@ -16,31 +16,35 @@ BEGIN accountB := vibetype_test.account_registration_verified('username-b', 'email+b@example.com'); accountC := vibetype_test.account_registration_verified('username-c', 'email+c@example.com'); - PERFORM vibetype_test.friendship_request_test('before A sends request to B', accountA, accountB, false); + PERFORM vibetype_test.friendship_request_test('before A sends request to B', accountA, accountA, accountB, false); -- friendship request from user A to B PERFORM vibetype_test.friendship_request(accountA, accountB, 'de'); - PERFORM vibetype_test.friendship_request_test('after A sends request to B (1)', accountA, accountB, true); - PERFORM vibetype_test.friendship_test('after A sends request to B (2)', accountA, accountB, null, 0); - PERFORM vibetype_test.friendship_test('after A sends request to B (3)', accountB, accountA, null, 0); + PERFORM vibetype_test.friendship_request_test('after A sends request to B (1)', accountA, accountA, accountB, true); + PERFORM vibetype_test.friendship_request_test('after A sends request to B (2)', accountB, accountA, accountB, true); + PERFORM vibetype_test.friendship_test('after A sends request to B (3)', accountA, accountA, accountB, null, 0); + PERFORM vibetype_test.friendship_test('after A sends request to B (4)', accountB, accountB, accountA, null, 0); + PERFORM vibetype_test.friendship_request_test('C cannot seen the friendship request from A to B', accountC, accountA, accountB, false); -- B accepts A's friendship request PERFORM vibetype_test.friendship_accept(accountB, accountA); - PERFORM vibetype_test.friendship_request_test('B accepts friendship request from A (1)', accountA, accountB, false); - PERFORM vibetype_test.friendship_test('B accepts friendship request from A (2)', accountA, accountB, false, 1); - PERFORM vibetype_test.friendship_test('B accepts friendship request from A (3)', accountB, accountA, false, 1); + PERFORM vibetype_test.friendship_request_test('B accepts friendship request from A (1)', accountA, accountA, accountB, false); + PERFORM vibetype_test.friendship_test('B accepts friendship request from A (2)', accountA, accountA, accountB, false, 1); + PERFORM vibetype_test.friendship_test('B accepts friendship request from A (3)', accountB, accountB, accountA, false, 1); + PERFORM vibetype_test.friendship_test('C cannot seen the friendship between A and B (1)', accountC, accountA, accountB, null, 0); + PERFORM vibetype_test.friendship_test('C cannot seen the friendship between A and B (2)', accountC, accountB, accountA, null, 0); -- friendship request from user C to A PERFORM vibetype_test.friendship_request(accountC, accountA, 'de'); - PERFORM vibetype_test.friendship_request_test('after C sends request to A (1)', accountC, accountA, true); - PERFORM vibetype_test.friendship_test('after C sends request to A (2)', accountC, accountA, null, 0); - PERFORM vibetype_test.friendship_test('after A sends request to B (3)', accountA, accountC, null, 0); - PERFORM vibetype_test.friendship_test('User B is still a friend of user A (1)', accountA, accountB, null, 1); - PERFORM vibetype_test.friendship_test('User A is still a friend of user B (2)', accountB, accountA, null, 1); - PERFORM vibetype_test.friendship_test('User C has no friends', accountC, null, null, 0); + PERFORM vibetype_test.friendship_request_test('after C sends request to A (1)', accountC, accountC, accountA, true); + PERFORM vibetype_test.friendship_test('after C sends request to A (2)', accountC, accountC, accountA, null, 0); + PERFORM vibetype_test.friendship_test('after A sends request to B (3)', accountA, accountA, accountC, null, 0); + PERFORM vibetype_test.friendship_test('User B is still a friend of user A (1)', accountA, accountA, accountB, null, 1); + PERFORM vibetype_test.friendship_test('User A is still a friend of user B (2)', accountB, accountB, accountA, null, 1); + PERFORM vibetype_test.friendship_test('User C has no friends', accountC, accountC, null, null, 0); BEGIN -- C sends another request to A, should lead to exception VTREQ @@ -88,20 +92,21 @@ BEGIN -- A rejects friendship request from C PERFORM vibetype_test.friendship_reject(accountA, accountC); - PERFORM vibetype_test.friendship_test('After A rejected C''s friendship request (1)', accountC, accountA, null, 0); - PERFORM vibetype_test.friendship_test('After A rejected C''s friendship request (2)', accountA, accountC, null, 0); + PERFORM vibetype_test.friendship_test('After A rejected C''s friendship request (1)', accountA, accountC, accountA, null, 0); + PERFORM vibetype_test.friendship_test('After A rejected C''s friendship request (2)', accountC, accountC, accountA, null, 0); -- a new friendship request from user C to A, this time accepted by A PERFORM vibetype_test.friendship_request(accountC, accountA, 'de'); PERFORM vibetype_test.friendship_accept(accountA, accountC); - PERFORM vibetype_test.friendship_test('Count the number of A''s friends', accountA, null, null, 2); - PERFORM vibetype_test.friendship_test('C is a friend of A (1)', accountA, accountC, null, 1); - PERFORM vibetype_test.friendship_test('C is a friend of A (2)', accountC, accountA, null, 1); + PERFORM vibetype_test.friendship_test('Count the number of A''s friends', accountA, accountA, null, null, 2); + PERFORM vibetype_test.friendship_test('C is a friend of A (1)', accountA, accountA, accountC, null, 1); + PERFORM vibetype_test.friendship_test('C is a friend of A (2)', accountA, accountC, accountA, null, 1); + PERFORM vibetype_test.friendship_test('C is a friend of A (3)', accountC, accountA, accountC, null, 1); + PERFORM vibetype_test.friendship_test('C is a friend of A (4)', accountC, accountC, accountA, null, 1); - -- friendship request from user B to A + -- friendship request from user B to C PERFORM vibetype_test.friendship_request(accountB, accountC, 'de'); - -- B marks A as a close friend PERFORM vibetype_test.friendship_toggle_closeness(accountB, accountA); @@ -117,30 +122,34 @@ BEGIN END LOOP; */ - PERFORM vibetype_test.friendship_test('B marks A as a close friend (1)', accountB, accountA, true, 1); - PERFORM vibetype_test.friendship_test('B marks A as a close friend (2)', accountA, accountB, false, 1); - - -- B unmarks A as a close friend - PERFORM vibetype_test.friendship_toggle_closeness(accountB, accountA); - PERFORM vibetype_test.friendship_test('B marks A as a close friend (1)', accountB, accountA, false, 1); + PERFORM vibetype_test.friendship_test('B marks A as a close friend (1)', accountB, accountB, accountA, true, 1); + PERFORM vibetype_test.friendship_test('B marks A as a close friend (2)', accountB, accountA, accountB, false, 1); + PERFORM vibetype_test.friendship_test('B marks A as a close friend (3)', accountA, accountB, accountA, true, 1); + PERFORM vibetype_test.friendship_test('B marks A as a close friend (4)', accountA, accountA, accountB, false, 1); - -- C marks A as a close friend - PERFORM vibetype_test.friendship_toggle_closeness(accountC, accountA); + -- A marks B as a close friend + PERFORM vibetype_test.friendship_toggle_closeness(accountA, accountB); - -- A wants to find out, if A is a close friend of C. The result should be NULL. + PERFORM vibetype_test.friendship_test('A marks B as a close friend (1)', accountB, accountB, accountA, true, 1); + PERFORM vibetype_test.friendship_test('A marks B as a close friend (2)', accountB, accountA, accountB, true, 1); + PERFORM vibetype_test.friendship_test('A marks B as a close friend (3)', accountA, accountB, accountA, true, 1); + PERFORM vibetype_test.friendship_test('A marks B as a close friend (4)', accountA, accountA, accountB, true, 1); - SET LOCAL role = 'vibetype_account'; - EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || accountA || ''''; + -- B unmarks A as a close friend + PERFORM vibetype_test.friendship_toggle_closeness(accountB, accountA); - SELECT is_close_friend INTO _is_close_friend - FROM vibetype.friendship - WHERE account_id = accountC and friend_account_id = accountA; + PERFORM vibetype_test.friendship_test('B unmarks A as a close friend (1)', accountB, accountB, accountA, false, 1); + PERFORM vibetype_test.friendship_test('B unmarks A as a close friend (2)', accountB, accountA, accountB, true, 1); + PERFORM vibetype_test.friendship_test('B unmarks A as a close friend (3)', accountA, accountB, accountA, false, 1); + PERFORM vibetype_test.friendship_test('B unmarks A as a close friend (4)', accountA, accountA, accountB, true, 1); - IF _is_close_friend IS NOT NULL THEN - RAISE EXCEPTION 'Closeness should not be disclosed to A.'; - END IF; + -- A cancels friendship with C + PERFORM vibetype_test.friendship_cancel(accountA, accountC); - SET LOCAL ROLE NONE; + PERFORM vibetype_test.friendship_test('A cancels friendship with C (1)', accountA, accountC, accountA, null, 0); + PERFORM vibetype_test.friendship_test('A cancels friendship with C (2)', accountA, accountA, accountC, null, 0); + PERFORM vibetype_test.friendship_test('A cancels friendship with C (3)', accountC, accountC, accountA, null, 0); + PERFORM vibetype_test.friendship_test('A cancels friendship with C (4)', accountC, accountA, accountC, null, 0); END $$; diff --git a/test/logic/utility/model/friendship.sql b/test/logic/utility/model/friendship.sql index 56962275..d8d7f233 100644 --- a/test/logic/utility/model/friendship.sql +++ b/test/logic/utility/model/friendship.sql @@ -84,6 +84,7 @@ GRANT EXECUTE ON FUNCTION vibetype_test.friendship_toggle_closeness(UUID, UUID) CREATE OR REPLACE FUNCTION vibetype_test.friendship_test ( _test_case TEXT, _invoker_account_id UUID, + _account_id UUID, _friend_account_id UUID, -- _friend_account_id IS NULL means "any friend" _is_close_friend BOOLEAN, -- _is_close_friend IS NULL means "any boolean value" _expected_count INTEGER @@ -96,7 +97,7 @@ BEGIN SELECT count(*) INTO _result FROM vibetype.friendship - WHERE account_id = _invoker_account_id + WHERE account_id = _account_id AND (_friend_account_id IS NULL OR friend_account_id = _friend_account_id) AND (_is_close_friend IS NULL OR is_close_friend = _is_close_friend); @@ -107,12 +108,13 @@ BEGIN SET LOCAL ROLE NONE; END $$ LANGUAGE plpgsql; -GRANT EXECUTE ON FUNCTION vibetype_test.friendship_test(TEXT, UUID, UUID, BOOLEAN, INTEGER) TO vibetype_account; +GRANT EXECUTE ON FUNCTION vibetype_test.friendship_test(TEXT, UUID, UUID, UUID, BOOLEAN, INTEGER) TO vibetype_account; CREATE OR REPLACE FUNCTION vibetype_test.friendship_request_test ( _test_case TEXT, _invoker_account_id UUID, + _account_id UUID, _friend_account_id UUID, _expected_to_exist BOOLEAN ) RETURNS VOID AS $$ @@ -124,7 +126,7 @@ BEGIN SELECT id INTO _id FROM vibetype.friendship_request - WHERE account_id = _invoker_account_id + WHERE account_id = _account_id AND friend_account_id = _friend_account_id; IF _id IS NULL AND _expected_to_exist THEN @@ -138,4 +140,4 @@ BEGIN SET LOCAL ROLE NONE; END $$ LANGUAGE plpgsql; -GRANT EXECUTE ON FUNCTION vibetype_test.friendship_request_test(TEXT, UUID, UUID, BOOLEAN) TO vibetype_account; +GRANT EXECUTE ON FUNCTION vibetype_test.friendship_request_test(TEXT, UUID, UUID, UUID, BOOLEAN) TO vibetype_account; From 4fc2a2139a58a7ea94362d1c20ef37b4c7a347fa Mon Sep 17 00:00:00 2001 From: Sven Thelemann Date: Fri, 1 Aug 2025 16:16:48 +0200 Subject: [PATCH 4/8] feat(friendship)!: redesign table friendship In the table `friendship` the columns `a_account_id` and `b_account_id`were renamed to `account_id`and `friend_account_id`, a new column `is_close_friend` was added, the policies were updated. Several friendship related functions were added. Test scripts were updated accordingly. --- src/deploy/function_friendship.sql | 151 +++++++++ src/deploy/table_friendship.sql | 65 ++-- src/revert/function_friendship.sql | 9 + src/revert/table_friendship.sql | 7 +- src/sqitch.plan | 1 + src/verify/function_friendship.sql | 28 ++ src/verify/table_friendship.sql | 5 +- test/fixture/schema_vibetype.definition.sql | 305 ++++++++++++++++-- test/logic/scenario/model/friendship.sql | 98 ++++-- .../utility/model/account_registration.sql | 2 +- test/logic/utility/model/friendship.sql | 145 +++------ 11 files changed, 622 insertions(+), 194 deletions(-) create mode 100644 src/deploy/function_friendship.sql create mode 100644 src/revert/function_friendship.sql create mode 100644 src/verify/function_friendship.sql diff --git a/src/deploy/function_friendship.sql b/src/deploy/function_friendship.sql new file mode 100644 index 00000000..14d7137d --- /dev/null +++ b/src/deploy/function_friendship.sql @@ -0,0 +1,151 @@ +BEGIN; + +-- accept friendship request + +CREATE OR REPLACE FUNCTION vibetype.friendship_accept( + requestor_account_id UUID +) RETURNS VOID AS $$ +DECLARE + _friend_account_id UUID; + _count INTEGER; +BEGIN + + _friend_account_id := vibetype.invoker_account_id(); + + UPDATE vibetype.friendship SET + status = 'accepted'::vibetype.friendship_status + -- updated_by filled by trigger + WHERE account_id = requestor_account_id AND friend_account_id = _friend_account_id + AND status = 'requested'::vibetype.friendship_status; + + GET DIAGNOSTICS _count = ROW_COUNT; + IF _count = 0 THEN + RAISE EXCEPTION 'Friendship request does not exist' USING ERRCODE = 'VTFAC'; + END IF; + + INSERT INTO vibetype.friendship(account_id, friend_account_id, status, created_by) + VALUES (_friend_account_id, requestor_account_id, 'accepted'::vibetype.friendship_status, _friend_account_id); + +END; $$ LANGUAGE plpgsql SECURITY INVOKER; + +COMMENT ON FUNCTION vibetype.friendship_accept(UUID) IS 'Accepts a friendship request.\n\nError codes:\n- **VTFAC** when a corresponding friendship request does not exist.'; + +GRANT EXECUTE ON FUNCTION vibetype.friendship_accept(UUID) TO vibetype_account; + +-- reject or cancel friendship + +CREATE OR REPLACE FUNCTION vibetype.friendship_cancel( + friend_account_id UUID +) RETURNS VOID AS $$ +DECLARE + _account_id UUID; +BEGIN + + _account_id := vibetype.invoker_account_id(); + + DELETE FROM vibetype.friendship f + WHERE (account_id = _account_id AND f.friend_account_id = friendship_cancel.friend_account_id) + OR (account_id = friendship_cancel.friend_account_id AND f.friend_account_id = _account_id); + +END; $$ LANGUAGE plpgsql SECURITY INVOKER; + +COMMENT ON FUNCTION vibetype.friendship_cancel(UUID) IS 'Rejects or cancels a friendship (in both directions).'; + +GRANT EXECUTE ON FUNCTION vibetype.friendship_cancel(UUID) TO vibetype_account; + +-- create notification for a request + +CREATE OR REPLACE FUNCTION vibetype.friendship_notify_request( + friend_account_id UUID, + language TEXT +) RETURNS VOID AS $$ +BEGIN + + INSERT INTO vibetype_private.notification (channel, payload) + VALUES ( + 'friendship_request', + jsonb_pretty(jsonb_build_object( + 'data', jsonb_build_object( + 'requestor_account_id', vibetype.invoker_account_id(), + 'requestee_account_id', friendship_notify_request.friend_account_id + ), + 'template', jsonb_build_object('language', friendship_notify_request.language) + )) + ); + +END; $$ LANGUAGE plpgsql SECURITY DEFINER; + +COMMENT ON FUNCTION vibetype.friendship_notify_request(UUID, TEXT) IS 'Creates a notification for a friendship_request'; + +GRANT EXECUTE ON FUNCTION vibetype.friendship_notify_request(UUID, TEXT) TO vibetype_account; + +-- request friendship + +CREATE OR REPLACE FUNCTION vibetype.friendship_request( + friend_account_id UUID, + language TEXT +) RETURNS VOID AS $$ +DECLARE + _account_id UUID; +BEGIN + + _account_id := vibetype.invoker_account_id(); + + IF EXISTS( + SELECT 1 + FROM vibetype.friendship f + WHERE (f.account_id = _account_id AND f.friend_account_id = friendship_request.friend_account_id) + OR (f.account_id = friendship_request.friend_account_id AND f.friend_account_id = _account_id) + ) + THEN + RAISE EXCEPTION 'Friendship already exists or has already been requested.' USING ERRCODE = 'VTREQ'; + END IF; + + INSERT INTO vibetype.friendship(account_id, friend_account_id, status, created_by) + VALUES (_account_id, friendship_request.friend_account_id, 'requested'::vibetype.friendship_status, _account_id); + + PERFORM vibetype.friendship_notify_request(friendship_request.friend_account_id, friendship_request.language); + +END; $$ LANGUAGE plpgsql SECURITY INVOKER; + +COMMENT ON FUNCTION vibetype.friendship_request(UUID, TEXT) IS 'Starts a new friendship request.\n\nError codes:\n- **VTREQ** when the friendship already exists or has already been requested.'; + +GRANT EXECUTE ON FUNCTION vibetype.friendship_request(UUID, TEXT) TO vibetype_account; + + +-- toggle closeness of friendship + +CREATE OR REPLACE FUNCTION vibetype.friendship_toggle_closeness( + friend_account_id UUID +) RETURNS BOOLEAN AS $$ +DECLARE + _account_id UUID; + _is_close_friend BOOLEAN; + current_status vibetype.friendship_status; +BEGIN + + _account_id := vibetype.invoker_account_id(); + + SELECT status INTO current_status + FROM vibetype.friendship f + WHERE f.account_id = _account_id AND f.friend_account_id = friendship_toggle_closeness.friend_account_id; + + IF current_status IS NULL OR current_status != 'accepted'::vibetype.friendship_status THEN + RAISE EXCEPTION 'Friendship does not exist' USING ERRCODE = 'VTFTC'; + END IF; + + UPDATE vibetype.friendship f + SET is_close_friend = NOT is_close_friend + WHERE account_id = vibetype.invoker_account_id() + AND f.friend_account_id = friendship_toggle_closeness.friend_account_id + RETURNING is_close_friend INTO _is_close_friend; + + RETURN _is_close_friend; + +END; $$ LANGUAGE plpgsql SECURITY INVOKER; + +COMMENT ON FUNCTION vibetype.friendship_toggle_closeness(UUID) IS 'Toggles a frien1dship relation between ''not a close friend'' and ''close friend''.\n\nError codes:\n- **VTFTC** when the friendship does not exist.'; + +GRANT EXECUTE ON FUNCTION vibetype.friendship_toggle_closeness(UUID) TO vibetype_account; + +COMMIT; diff --git a/src/deploy/table_friendship.sql b/src/deploy/table_friendship.sql index 4744ce89..2553f04c 100644 --- a/src/deploy/table_friendship.sql +++ b/src/deploy/table_friendship.sql @@ -1,10 +1,10 @@ -BEGIN; - CREATE TABLE vibetype.friendship ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - a_account_id UUID NOT NULL REFERENCES vibetype.account(id) ON DELETE CASCADE, - b_account_id UUID NOT NULL REFERENCES vibetype.account(id) ON DELETE CASCADE, + account_id UUID NOT NULL REFERENCES vibetype.account(id) ON DELETE CASCADE, + friend_account_id UUID NOT NULL REFERENCES vibetype.account(id) ON DELETE CASCADE, + + is_close_friend BOOLEAN NOT NULL DEFAULT false, status vibetype.friendship_status NOT NULL DEFAULT 'requested'::vibetype.friendship_status, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -12,11 +12,9 @@ CREATE TABLE vibetype.friendship ( updated_at TIMESTAMP WITH TIME ZONE, updated_by UUID REFERENCES vibetype.account(id) ON DELETE SET NULL, - UNIQUE (a_account_id, b_account_id), - CONSTRAINT friendship_creator_participant CHECK (created_by = a_account_id or created_by = b_account_id), - CONSTRAINT friendship_creator_updater_difference CHECK (created_by <> updated_by), - CONSTRAINT friendship_ordering CHECK (a_account_id < b_account_id), - CONSTRAINT friendship_updater_participant CHECK (updated_by IS NULL or updated_by = a_account_id or updated_by = b_account_id) + UNIQUE (account_id, friend_account_id), + CONSTRAINT friendship_creator_friend CHECK (account_id <> friend_account_id), + CONSTRAINT friendship_creator_participant CHECK (created_by = account_id) ); CREATE INDEX idx_friendship_created_by ON vibetype.friendship USING btree (created_by); @@ -24,8 +22,9 @@ CREATE INDEX idx_friendship_updated_by ON vibetype.friendship USING btree (updat COMMENT ON TABLE vibetype.friendship IS 'A friend relation together with its status.'; COMMENT ON COLUMN vibetype.friendship.id IS E'@omit create,update\nThe friend relation''s internal id.'; -COMMENT ON COLUMN vibetype.friendship.a_account_id IS E'@omit update\nThe ''left'' side of the friend relation. It must be lexically less than the ''right'' side.'; -COMMENT ON COLUMN vibetype.friendship.b_account_id IS E'@omit update\nThe ''right'' side of the friend relation. It must be lexically greater than the ''left'' side.'; +COMMENT ON COLUMN vibetype.friendship.account_id IS E'@omit update\nThe one side of the friend relation. If the status is ''requested'' then it is the requestor account.'; +COMMENT ON COLUMN vibetype.friendship.friend_account_id IS E'@omit update\nThe other side of the friend relation. If the status is ''requested'' then it is the requestee account.'; +COMMENT ON COLUMN vibetype.friendship.is_close_friend IS E'@omit create\nThe flag indicating whether account_id considers friend_account_id as a close friend or not.'; COMMENT ON COLUMN vibetype.friendship.status IS E'@omit create\nThe status of the friend relation.'; COMMENT ON COLUMN vibetype.friendship.created_at IS E'@omit create,update\nThe timestamp when the friend relation was created.'; COMMENT ON COLUMN vibetype.friendship.created_by IS E'@omit update\nThe account that created the friend relation was created.'; @@ -45,20 +44,19 @@ GRANT INSERT, SELECT, UPDATE, DELETE ON TABLE vibetype.friendship TO vibetype_ac ALTER TABLE vibetype.friendship ENABLE ROW LEVEL SECURITY; +CREATE POLICY friendship_not_blocked ON vibetype.friendship FOR ALL +USING ( + account_id NOT IN (SELECT id FROM vibetype_private.account_block_ids()) + AND friend_account_id NOT IN (SELECT id FROM vibetype_private.account_block_ids()) +); + -- Only allow interactions with friendships in which the current user is involved. -CREATE POLICY friendship_existing ON vibetype.friendship FOR ALL +CREATE POLICY friendship_select ON vibetype.friendship FOR SELECT USING ( - ( - vibetype.invoker_account_id() = a_account_id - AND b_account_id NOT IN (SELECT id FROM vibetype_private.account_block_ids()) - ) + account_id = vibetype.invoker_account_id() OR - ( - vibetype.invoker_account_id() = b_account_id - AND a_account_id NOT IN (SELECT id FROM vibetype_private.account_block_ids()) - ) -) -WITH CHECK (FALSE); + friend_account_id = vibetype.invoker_account_id() +); -- Only allow creation by the current user. CREATE POLICY friendship_insert ON vibetype.friendship FOR INSERT @@ -66,9 +64,11 @@ WITH CHECK ( created_by = vibetype.invoker_account_id() ); --- Only allow update by the current user and only the state transition requested -> accepted. -CREATE POLICY friendship_update ON vibetype.friendship FOR UPDATE +-- Only allow update by the current user if it is about accepting a friendship request. +CREATE POLICY friendship_update_accept ON vibetype.friendship FOR UPDATE USING ( + friend_account_id = vibetype.invoker_account_id() + AND status = 'requested'::vibetype.friendship_status ) WITH CHECK ( status = 'accepted'::vibetype.friendship_status @@ -76,4 +76,19 @@ USING ( updated_by = vibetype.invoker_account_id() ); -COMMIT; +-- Only allow update by the current user if it is already an accepted relation. +CREATE POLICY friendship_update_toggle_closeness ON vibetype.friendship FOR UPDATE +USING ( + status = 'accepted'::vibetype.friendship_status + AND + account_id = vibetype.invoker_account_id() +) WITH CHECK ( + status = 'accepted'::vibetype.friendship_status + AND + updated_by = vibetype.invoker_account_id() +); + +CREATE POLICY friendship_delete ON vibetype.friendship FOR DELETE +USING ( + TRUE +); diff --git a/src/revert/function_friendship.sql b/src/revert/function_friendship.sql new file mode 100644 index 00000000..db7c86a7 --- /dev/null +++ b/src/revert/function_friendship.sql @@ -0,0 +1,9 @@ +BEGIN; + +DROP FUNCTION vibetype.friendship_accept(UUID); +DROP FUNCTION vibetype.friendship_cancel(UUID); +DROP FUNCTION vibetype.friendship_notify_request(UUID, TEXT); +DROP FUNCTION vibetype.friendship_request(UUID, TEXT); +DROP FUNCTION vibetype.friendship_toggle_closeness(UUID); + +COMMIT; diff --git a/src/revert/table_friendship.sql b/src/revert/table_friendship.sql index 8d3a165d..17413551 100644 --- a/src/revert/table_friendship.sql +++ b/src/revert/table_friendship.sql @@ -1,8 +1,11 @@ BEGIN; -DROP POLICY friendship_update ON vibetype.friendship; +DROP POLICY friendship_not_blocked ON vibetype.friendship; +DROP POLICY friendship_select ON vibetype.friendship; DROP POLICY friendship_insert ON vibetype.friendship; -DROP POLICY friendship_existing ON vibetype.friendship; +DROP POLICY friendship_update_accept ON vibetype.friendship; +DROP POLICY friendship_update_toggle_closeness ON vibetype.friendship; +DROP POLICY friendship_delete ON vibetype.friendship; DROP TRIGGER vibetype_trigger_friendship_update ON vibetype.friendship; diff --git a/src/sqitch.plan b/src/sqitch.plan index fd0dbbf2..18ee931b 100644 --- a/src/sqitch.plan +++ b/src/sqitch.plan @@ -95,3 +95,4 @@ table_preference_event_location [schema_public table_account_public role_account role_zammad 1970-01-01T00:00:00Z Sven Thelemann # Add role zammad. database_zammad [role_zammad] 1970-01-01T00:00:00Z Sven Thelemann # Add the database for zammad. function_account_search [schema_public table_account_public role_account] 1970-01-01T00:00:00Z Svens Thelemann # Add a function for searching accounts based on a substring query. +function_friendship [schema_public table_friendship role_account] 1970-01-01T00:00:00Z Svens Thelemann # Add functions for handling friendships. diff --git a/src/verify/function_friendship.sql b/src/verify/function_friendship.sql new file mode 100644 index 00000000..5a922070 --- /dev/null +++ b/src/verify/function_friendship.sql @@ -0,0 +1,28 @@ +BEGIN; + +DO $$ +BEGIN + + IF NOT (SELECT pg_catalog.has_function_privilege('vibetype_account', 'vibetype.friendship_accept(UUID)', 'EXECUTE')) THEN + RAISE EXCEPTION 'Test failed: vibetype_account does not have EXECUTE privilege for vibetype.friendship_accept(UUID).'; + END IF; + + IF NOT (SELECT pg_catalog.has_function_privilege('vibetype_account', 'vibetype.friendship_cancel(UUID)', 'EXECUTE')) THEN + RAISE EXCEPTION 'Test failed: vibetype_account does not have EXECUTE privilege for vibetype.friendship_cancel(UUID).'; + END IF; + + IF NOT (SELECT pg_catalog.has_function_privilege('vibetype_account', 'vibetype.friendship_notify_request(UUID, TEXT)', 'EXECUTE')) THEN + RAISE EXCEPTION 'Test failed: vibetype_account does not have EXECUTE privilege for vibetype.friendship_notify_request(UUID, TEXT).'; + END IF; + + IF NOT (SELECT pg_catalog.has_function_privilege('vibetype_account', 'vibetype.friendship_request(UUID, TEXT)', 'EXECUTE')) THEN + RAISE EXCEPTION 'Test failed: vibetype_account does not have EXECUTE privilege for vibetype.friendship_request(UUID, TEXT).'; + END IF; + + IF NOT (SELECT pg_catalog.has_function_privilege('vibetype_account', 'vibetype.friendship_toggle_closeness(UUID)', 'EXECUTE')) THEN + RAISE EXCEPTION 'Test failed: vibetype_account does not have EXECUTE privilege for vibetype.friendship_toggle_closeness(UUID).'; + END IF; + +END $$; + +ROLLBACK; diff --git a/src/verify/table_friendship.sql b/src/verify/table_friendship.sql index 66005c24..b57e3a90 100644 --- a/src/verify/table_friendship.sql +++ b/src/verify/table_friendship.sql @@ -2,8 +2,9 @@ BEGIN; SELECT id, - a_account_id, - b_account_id, + account_id, + friend_account_id, + is_close_friend, status, created_at, created_by, diff --git a/test/fixture/schema_vibetype.definition.sql b/test/fixture/schema_vibetype.definition.sql index 90a680d7..7430cf3a 100644 --- a/test/fixture/schema_vibetype.definition.sql +++ b/test/fixture/schema_vibetype.definition.sql @@ -1310,6 +1310,191 @@ ALTER FUNCTION vibetype.events_organized() OWNER TO ci; COMMENT ON FUNCTION vibetype.events_organized() IS 'Add a function that returns all event ids for which the invoker is the creator.'; +-- +-- Name: friendship_accept(uuid); Type: FUNCTION; Schema: vibetype; Owner: ci +-- + +CREATE FUNCTION vibetype.friendship_accept(requestor_account_id uuid) RETURNS void + LANGUAGE plpgsql + AS $$ +DECLARE + _friend_account_id UUID; + _count INTEGER; +BEGIN + + _friend_account_id := vibetype.invoker_account_id(); + + UPDATE vibetype.friendship SET + status = 'accepted'::vibetype.friendship_status + -- updated_by filled by trigger + WHERE account_id = requestor_account_id AND friend_account_id = _friend_account_id + AND status = 'requested'::vibetype.friendship_status; + + GET DIAGNOSTICS _count = ROW_COUNT; + IF _count = 0 THEN + RAISE EXCEPTION 'Friendship request does not exist' USING ERRCODE = 'VTFAC'; + END IF; + + INSERT INTO vibetype.friendship(account_id, friend_account_id, status, created_by) + VALUES (_friend_account_id, requestor_account_id, 'accepted'::vibetype.friendship_status, _friend_account_id); + +END; $$; + + +ALTER FUNCTION vibetype.friendship_accept(requestor_account_id uuid) OWNER TO ci; + +-- +-- Name: FUNCTION friendship_accept(requestor_account_id uuid); Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON FUNCTION vibetype.friendship_accept(requestor_account_id uuid) IS 'Accepts a friendship request.\n\nError codes:\n- **VTFAC** when a corresponding friendship request does not exist.'; + + +-- +-- Name: friendship_cancel(uuid); Type: FUNCTION; Schema: vibetype; Owner: ci +-- + +CREATE FUNCTION vibetype.friendship_cancel(friend_account_id uuid) RETURNS void + LANGUAGE plpgsql + AS $$ +DECLARE + _account_id UUID; +BEGIN + + _account_id := vibetype.invoker_account_id(); + + DELETE FROM vibetype.friendship f + WHERE (account_id = _account_id AND f.friend_account_id = friendship_cancel.friend_account_id) + OR (account_id = friendship_cancel.friend_account_id AND f.friend_account_id = _account_id); + +END; $$; + + +ALTER FUNCTION vibetype.friendship_cancel(friend_account_id uuid) OWNER TO ci; + +-- +-- Name: FUNCTION friendship_cancel(friend_account_id uuid); Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON FUNCTION vibetype.friendship_cancel(friend_account_id uuid) IS 'Rejects or cancels a friendship (in both directions).'; + + +-- +-- Name: friendship_notify_request(uuid, text); Type: FUNCTION; Schema: vibetype; Owner: ci +-- + +CREATE FUNCTION vibetype.friendship_notify_request(friend_account_id uuid, language text) RETURNS void + LANGUAGE plpgsql SECURITY DEFINER + AS $$ +BEGIN + + INSERT INTO vibetype_private.notification (channel, payload) + VALUES ( + 'friendship_request', + jsonb_pretty(jsonb_build_object( + 'data', jsonb_build_object( + 'requestor_account_id', vibetype.invoker_account_id(), + 'requestee_account_id', friendship_notify_request.friend_account_id + ), + 'template', jsonb_build_object('language', friendship_notify_request.language) + )) + ); + +END; $$; + + +ALTER FUNCTION vibetype.friendship_notify_request(friend_account_id uuid, language text) OWNER TO ci; + +-- +-- Name: FUNCTION friendship_notify_request(friend_account_id uuid, language text); Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON FUNCTION vibetype.friendship_notify_request(friend_account_id uuid, language text) IS 'Creates a notification for a friendship_request'; + + +-- +-- Name: friendship_request(uuid, text); Type: FUNCTION; Schema: vibetype; Owner: ci +-- + +CREATE FUNCTION vibetype.friendship_request(friend_account_id uuid, language text) RETURNS void + LANGUAGE plpgsql + AS $$ +DECLARE + _account_id UUID; +BEGIN + + _account_id := vibetype.invoker_account_id(); + + IF EXISTS( + SELECT 1 + FROM vibetype.friendship f + WHERE (f.account_id = _account_id AND f.friend_account_id = friendship_request.friend_account_id) + OR (f.account_id = friendship_request.friend_account_id AND f.friend_account_id = _account_id) + ) + THEN + RAISE EXCEPTION 'Friendship already exists or has already been requested.' USING ERRCODE = 'VTREQ'; + END IF; + + INSERT INTO vibetype.friendship(account_id, friend_account_id, status, created_by) + VALUES (_account_id, friendship_request.friend_account_id, 'requested'::vibetype.friendship_status, _account_id); + + PERFORM vibetype.friendship_notify_request(friendship_request.friend_account_id, friendship_request.language); + +END; $$; + + +ALTER FUNCTION vibetype.friendship_request(friend_account_id uuid, language text) OWNER TO ci; + +-- +-- Name: FUNCTION friendship_request(friend_account_id uuid, language text); Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON FUNCTION vibetype.friendship_request(friend_account_id uuid, language text) IS 'Starts a new friendship request.\n\nError codes:\n- **VTREQ** when the friendship already exists or has already been requested.'; + + +-- +-- Name: friendship_toggle_closeness(uuid); Type: FUNCTION; Schema: vibetype; Owner: ci +-- + +CREATE FUNCTION vibetype.friendship_toggle_closeness(friend_account_id uuid) RETURNS boolean + LANGUAGE plpgsql + AS $$ +DECLARE + _account_id UUID; + _is_close_friend BOOLEAN; + current_status vibetype.friendship_status; +BEGIN + + _account_id := vibetype.invoker_account_id(); + + SELECT status INTO current_status + FROM vibetype.friendship f + WHERE f.account_id = _account_id AND f.friend_account_id = friendship_toggle_closeness.friend_account_id; + + IF current_status IS NULL OR current_status != 'accepted'::vibetype.friendship_status THEN + RAISE EXCEPTION 'Friendship does not exist' USING ERRCODE = 'VTFTC'; + END IF; + + UPDATE vibetype.friendship f + SET is_close_friend = NOT is_close_friend + WHERE account_id = vibetype.invoker_account_id() + AND f.friend_account_id = friendship_toggle_closeness.friend_account_id + RETURNING is_close_friend INTO _is_close_friend; + + RETURN _is_close_friend; + +END; $$; + + +ALTER FUNCTION vibetype.friendship_toggle_closeness(friend_account_id uuid) OWNER TO ci; + +-- +-- Name: FUNCTION friendship_toggle_closeness(friend_account_id uuid); Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON FUNCTION vibetype.friendship_toggle_closeness(friend_account_id uuid) IS 'Toggles a frien1dship relation between ''not a close friend'' and ''close friend''.\n\nError codes:\n- **VTFTC** when the friendship does not exist.'; + + -- -- Name: guest_claim_array(); Type: FUNCTION; Schema: vibetype; Owner: ci -- @@ -3313,17 +3498,16 @@ Reference to the uploaded file.'; CREATE TABLE vibetype.friendship ( id uuid DEFAULT gen_random_uuid() NOT NULL, - a_account_id uuid NOT NULL, - b_account_id uuid NOT NULL, + account_id uuid NOT NULL, + friend_account_id uuid NOT NULL, + is_close_friend boolean DEFAULT false NOT NULL, status vibetype.friendship_status DEFAULT 'requested'::vibetype.friendship_status NOT NULL, created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, created_by uuid NOT NULL, updated_at timestamp with time zone, updated_by uuid, - CONSTRAINT friendship_creator_participant CHECK (((created_by = a_account_id) OR (created_by = b_account_id))), - CONSTRAINT friendship_creator_updater_difference CHECK ((created_by <> updated_by)), - CONSTRAINT friendship_ordering CHECK ((a_account_id < b_account_id)), - CONSTRAINT friendship_updater_participant CHECK (((updated_by IS NULL) OR (updated_by = a_account_id) OR (updated_by = b_account_id))) + CONSTRAINT friendship_creator_friend CHECK ((account_id <> friend_account_id)), + CONSTRAINT friendship_creator_participant CHECK ((created_by = account_id)) ); @@ -3345,19 +3529,27 @@ The friend relation''s internal id.'; -- --- Name: COLUMN friendship.a_account_id; Type: COMMENT; Schema: vibetype; Owner: ci +-- Name: COLUMN friendship.account_id; Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON COLUMN vibetype.friendship.account_id IS '@omit update +The one side of the friend relation. If the status is ''requested'' then it is the requestor account.'; + + +-- +-- Name: COLUMN friendship.friend_account_id; Type: COMMENT; Schema: vibetype; Owner: ci -- -COMMENT ON COLUMN vibetype.friendship.a_account_id IS '@omit update -The ''left'' side of the friend relation. It must be lexically less than the ''right'' side.'; +COMMENT ON COLUMN vibetype.friendship.friend_account_id IS '@omit update +The other side of the friend relation. If the status is ''requested'' then it is the requestee account.'; -- --- Name: COLUMN friendship.b_account_id; Type: COMMENT; Schema: vibetype; Owner: ci +-- Name: COLUMN friendship.is_close_friend; Type: COMMENT; Schema: vibetype; Owner: ci -- -COMMENT ON COLUMN vibetype.friendship.b_account_id IS '@omit update -The ''right'' side of the friend relation. It must be lexically greater than the ''left'' side.'; +COMMENT ON COLUMN vibetype.friendship.is_close_friend IS '@omit create +The flag indicating whether account_id considers friend_account_id as a close friend or not.'; -- @@ -4604,11 +4796,11 @@ ALTER TABLE ONLY vibetype.event_upload -- --- Name: friendship friendship_a_account_id_b_account_id_key; Type: CONSTRAINT; Schema: vibetype; Owner: ci +-- Name: friendship friendship_account_id_friend_account_id_key; Type: CONSTRAINT; Schema: vibetype; Owner: ci -- ALTER TABLE ONLY vibetype.friendship - ADD CONSTRAINT friendship_a_account_id_b_account_id_key UNIQUE (a_account_id, b_account_id); + ADD CONSTRAINT friendship_account_id_friend_account_id_key UNIQUE (account_id, friend_account_id); -- @@ -5281,27 +5473,27 @@ ALTER TABLE ONLY vibetype.event_upload -- --- Name: friendship friendship_a_account_id_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci +-- Name: friendship friendship_account_id_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci -- ALTER TABLE ONLY vibetype.friendship - ADD CONSTRAINT friendship_a_account_id_fkey FOREIGN KEY (a_account_id) REFERENCES vibetype.account(id) ON DELETE CASCADE; + ADD CONSTRAINT friendship_account_id_fkey FOREIGN KEY (account_id) REFERENCES vibetype.account(id) ON DELETE CASCADE; -- --- Name: friendship friendship_b_account_id_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci +-- Name: friendship friendship_created_by_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci -- ALTER TABLE ONLY vibetype.friendship - ADD CONSTRAINT friendship_b_account_id_fkey FOREIGN KEY (b_account_id) REFERENCES vibetype.account(id) ON DELETE CASCADE; + ADD CONSTRAINT friendship_created_by_fkey FOREIGN KEY (created_by) REFERENCES vibetype.account(id) ON DELETE CASCADE; -- --- Name: friendship friendship_created_by_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci +-- Name: friendship friendship_friend_account_id_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci -- ALTER TABLE ONLY vibetype.friendship - ADD CONSTRAINT friendship_created_by_fkey FOREIGN KEY (created_by) REFERENCES vibetype.account(id) ON DELETE CASCADE; + ADD CONSTRAINT friendship_friend_account_id_fkey FOREIGN KEY (friend_account_id) REFERENCES vibetype.account(id) ON DELETE CASCADE; -- @@ -5750,12 +5942,10 @@ CREATE POLICY event_upload_select ON vibetype.event_upload FOR SELECT USING ((ev ALTER TABLE vibetype.friendship ENABLE ROW LEVEL SECURITY; -- --- Name: friendship friendship_existing; Type: POLICY; Schema: vibetype; Owner: ci +-- Name: friendship friendship_delete; Type: POLICY; Schema: vibetype; Owner: ci -- -CREATE POLICY friendship_existing ON vibetype.friendship USING ((((vibetype.invoker_account_id() = a_account_id) AND (NOT (b_account_id IN ( SELECT account_block_ids.id - FROM vibetype_private.account_block_ids() account_block_ids(id))))) OR ((vibetype.invoker_account_id() = b_account_id) AND (NOT (a_account_id IN ( SELECT account_block_ids.id - FROM vibetype_private.account_block_ids() account_block_ids(id))))))) WITH CHECK (false); +CREATE POLICY friendship_delete ON vibetype.friendship FOR DELETE USING (true); -- @@ -5766,10 +5956,33 @@ CREATE POLICY friendship_insert ON vibetype.friendship FOR INSERT WITH CHECK ((c -- --- Name: friendship friendship_update; Type: POLICY; Schema: vibetype; Owner: ci +-- Name: friendship friendship_not_blocked; Type: POLICY; Schema: vibetype; Owner: ci -- -CREATE POLICY friendship_update ON vibetype.friendship FOR UPDATE USING ((status = 'requested'::vibetype.friendship_status)) WITH CHECK (((status = 'accepted'::vibetype.friendship_status) AND (updated_by = vibetype.invoker_account_id()))); +CREATE POLICY friendship_not_blocked ON vibetype.friendship USING (((NOT (account_id IN ( SELECT account_block_ids.id + FROM vibetype_private.account_block_ids() account_block_ids(id)))) AND (NOT (friend_account_id IN ( SELECT account_block_ids.id + FROM vibetype_private.account_block_ids() account_block_ids(id)))))); + + +-- +-- Name: friendship friendship_select; Type: POLICY; Schema: vibetype; Owner: ci +-- + +CREATE POLICY friendship_select ON vibetype.friendship FOR SELECT USING (((account_id = vibetype.invoker_account_id()) OR (friend_account_id = vibetype.invoker_account_id()))); + + +-- +-- Name: friendship friendship_update_accept; Type: POLICY; Schema: vibetype; Owner: ci +-- + +CREATE POLICY friendship_update_accept ON vibetype.friendship FOR UPDATE USING (((friend_account_id = vibetype.invoker_account_id()) AND (status = 'requested'::vibetype.friendship_status))) WITH CHECK (((status = 'accepted'::vibetype.friendship_status) AND (updated_by = vibetype.invoker_account_id()))); + + +-- +-- Name: friendship friendship_update_toggle_closeness; Type: POLICY; Schema: vibetype; Owner: ci +-- + +CREATE POLICY friendship_update_toggle_closeness ON vibetype.friendship FOR UPDATE USING (((status = 'accepted'::vibetype.friendship_status) AND (account_id = vibetype.invoker_account_id()))) WITH CHECK (((status = 'accepted'::vibetype.friendship_status) AND (updated_by = vibetype.invoker_account_id()))); -- @@ -6197,6 +6410,46 @@ GRANT ALL ON FUNCTION vibetype.events_organized() TO vibetype_account; GRANT ALL ON FUNCTION vibetype.events_organized() TO vibetype_anonymous; +-- +-- Name: FUNCTION friendship_accept(requestor_account_id uuid); Type: ACL; Schema: vibetype; Owner: ci +-- + +REVOKE ALL ON FUNCTION vibetype.friendship_accept(requestor_account_id uuid) FROM PUBLIC; +GRANT ALL ON FUNCTION vibetype.friendship_accept(requestor_account_id uuid) TO vibetype_account; + + +-- +-- Name: FUNCTION friendship_cancel(friend_account_id uuid); Type: ACL; Schema: vibetype; Owner: ci +-- + +REVOKE ALL ON FUNCTION vibetype.friendship_cancel(friend_account_id uuid) FROM PUBLIC; +GRANT ALL ON FUNCTION vibetype.friendship_cancel(friend_account_id uuid) TO vibetype_account; + + +-- +-- Name: FUNCTION friendship_notify_request(friend_account_id uuid, language text); Type: ACL; Schema: vibetype; Owner: ci +-- + +REVOKE ALL ON FUNCTION vibetype.friendship_notify_request(friend_account_id uuid, language text) FROM PUBLIC; +GRANT ALL ON FUNCTION vibetype.friendship_notify_request(friend_account_id uuid, language text) TO vibetype_account; + + +-- +-- Name: FUNCTION friendship_request(friend_account_id uuid, language text); Type: ACL; Schema: vibetype; Owner: ci +-- + +REVOKE ALL ON FUNCTION vibetype.friendship_request(friend_account_id uuid, language text) FROM PUBLIC; +GRANT ALL ON FUNCTION vibetype.friendship_request(friend_account_id uuid, language text) TO vibetype_account; + + +-- +-- Name: FUNCTION friendship_toggle_closeness(friend_account_id uuid); Type: ACL; Schema: vibetype; Owner: ci +-- + +REVOKE ALL ON FUNCTION vibetype.friendship_toggle_closeness(friend_account_id uuid) FROM PUBLIC; +GRANT ALL ON FUNCTION vibetype.friendship_toggle_closeness(friend_account_id uuid) TO vibetype_account; + + -- -- Name: FUNCTION guest_claim_array(); Type: ACL; Schema: vibetype; Owner: ci -- diff --git a/test/logic/scenario/model/friendship.sql b/test/logic/scenario/model/friendship.sql index b6b6d358..4ced4c97 100644 --- a/test/logic/scenario/model/friendship.sql +++ b/test/logic/scenario/model/friendship.sql @@ -7,9 +7,7 @@ DECLARE accountA UUID; accountB UUID; accountC UUID; - friendshipAB UUID; - friendshipAC UUID; - friendshipCA UUID; + rec RECORD; BEGIN -- before all accountA := vibetype_test.account_registration_verified('username-a', 'email+a@example.com'); @@ -17,41 +15,81 @@ BEGIN accountC := vibetype_test.account_registration_verified('username-c', 'email+c@example.com'); -- friendship request from user A to B - friendshipAB := vibetype_test.friendship_request(accountA, accountB); - PERFORM vibetype_test.friendship_test('The friendship is requested for user A', accountA, 'requested', ARRAY[friendshipAB]::UUID[]); - PERFORM vibetype_test.friendship_test('The friendship is requested for user B', accountB, 'requested', ARRAY[friendshipAB]::UUID[]); - PERFORM vibetype_test.friendship_account_ids_test('User A has no friends', accountA, ARRAY[]::UUID[]); - PERFORM vibetype_test.friendship_account_ids_test('User B has no friends', accountB, ARRAY[]::UUID[]); - - -- friendship acceptance - PERFORM vibetype_test.friendship_accept(accountB, friendshipAB); - PERFORM vibetype_test.friendship_test('The friendship is accepted for user A', accountA, 'accepted', ARRAY[friendshipAB]::UUID[]); - PERFORM vibetype_test.friendship_test('The friendship is accepted for user B', accountB, 'accepted', ARRAY[friendshipAB]::UUID[]); - PERFORM vibetype_test.friendship_test('There is no requested friendship for user A', accountA, 'requested', ARRAY[]::UUID[]); - PERFORM vibetype_test.friendship_test('There is no requested friendship for user B', accountA, 'requested', ARRAY[]::UUID[]); - PERFORM vibetype_test.friendship_account_ids_test('User B is a friend of user A', accountA, ARRAY[accountB]::UUID[]); - PERFORM vibetype_test.friendship_account_ids_test('User A is a friend of user B', accountB, ARRAY[accountA]::UUID[]); + PERFORM vibetype_test.friendship_request(accountA, accountB, 'de'); + + + RAISE NOTICE '----'; + FOR rec IN + SELECT a.username, b.username as friend_username, f.is_close_friend, f.status + FROM vibetype.friendship f + JOIN vibetype.account a ON f.account_id = a.id + JOIN vibetype.account b ON f.friend_account_id = b.id + LOOP + RAISE NOTICE 'friendship: account = %, friend_account = %, is_close_friend = %, status = %', rec.username, rec.friend_username, rec.is_close_friend, rec.status; + END LOOP; + + PERFORM vibetype_test.friendship_test('A sends B a friendship request (1)', accountA, accountB, false, 'requested', 1); + PERFORM vibetype_test.friendship_test('A sends B a friendship request (2)', accountB, accountA, false, 'requested', 0); + PERFORM vibetype_test.friendship_test('A sends B a friendship request (3)', accountA, accountB, false, null, 1); + PERFORM vibetype_test.friendship_test('A sends B a friendship request (3)', accountB, accountA, false, null, 0); + + -- B accepts A's friendship request + PERFORM vibetype_test.friendship_accept(accountB, accountA); + + PERFORM vibetype_test.friendship_test('B accepts friendship request from A (1)', accountA, accountB, false, 'requested', 0); + PERFORM vibetype_test.friendship_test('B accepts friendship request from A (2)', accountA, accountB, false, 'accepted', 1); + PERFORM vibetype_test.friendship_test('B accepts friendship request from A (3)', accountB, accountA, false, 'accepted', 1); -- friendship request from user C to A - friendshipCA := vibetype_test.friendship_request(accountC, accountA); - PERFORM vibetype_test.friendship_test('There is still only one accepted friendship for user A', accountA, 'accepted', ARRAY[friendshipAB]::UUID[]); - PERFORM vibetype_test.friendship_test('There is a new requested friendship for user C', accountC, 'requested', ARRAY[friendshipCA]::UUID[]); - PERFORM vibetype_test.friendship_account_ids_test('User B is still a friend of user A', accountA, ARRAY[accountB]::UUID[]); - PERFORM vibetype_test.friendship_account_ids_test('User C has no friends', accountC, ARRAY[]::UUID[]); + PERFORM vibetype_test.friendship_request(accountC, accountA, 'de'); + + PERFORM vibetype_test.friendship_test('There is still only one accepted friendship for user A', accountA, null, false, 'accepted', 1); + PERFORM vibetype_test.friendship_test('There is a new requested friendship for user C (1)', accountC, null, false, 'requested', 1); + PERFORM vibetype_test.friendship_test('There is a new requested friendship for user C (2)', accountA, accountC, false, null, 0); + PERFORM vibetype_test.friendship_test('User B is still a friend of user A (1)', accountA, accountB, false, null, 1); + PERFORM vibetype_test.friendship_test('User B is still a friend of user A (2)', accountB, accountA, false, null, 1); + PERFORM vibetype_test.friendship_test('User C has no friends', accountC, null, false, 'accepted', 0); BEGIN - friendshipAC := vibetype_test.friendship_request(accountA, accountC); - RAISE 'It was possible to requested a friendship more than once.'; + PERFORM vibetype_test.friendship_request(accountA, accountC, 'de'); + RAISE 'It was possible to request a friendship more than once.'; EXCEPTION - WHEN unique_violation THEN -- do nothing as expected - WHEN OTHERS THEN RAISE; + WHEN OTHERS THEN + IF SQLSTATE != 'VTREQ' THEN + RAISE; + END IF; END; -- friendship rejection - PERFORM vibetype_test.friendship_reject(accountA, friendshipCA); - PERFORM vibetype_test.friendship_test('After user A rejected user C''s friendship request, the friendship is removed for user C', accountC, NULL, ARRAY[]::UUID[]); - PERFORM vibetype_test.friendship_account_ids_test('After user A rejected user C''s friendship request, user B is still a friend of user A', accountA, ARRAY[accountB]::UUID[]); - PERFORM vibetype_test.friendship_account_ids_test('After user A rejected user C''s friendship request, user C has no friends anymore', accountC, ARRAY[]::UUID[]); + PERFORM vibetype_test.friendship_cancel(accountA, accountC); + PERFORM vibetype_test.friendship_test('After user A rejected user C''s friendship request (1)', accountC, accountA, false, null, 0); + PERFORM vibetype_test.friendship_test('After user A rejected user C''s friendship request (2)', accountA, accountC, false, null, 0); + + -- a new friendship request from user C to A, this time accepted by A + PERFORM vibetype_test.friendship_request(accountC, accountA, 'de'); + PERFORM vibetype_test.friendship_accept(accountA, accountC); + PERFORM vibetype_test.friendship_test('Count the number of A''s friends', accountA, null, false, 'accepted', 2); + PERFORM vibetype_test.friendship_test('C is a friend of A (1)', accountA, accountC, false, 'accepted', 1); + PERFORM vibetype_test.friendship_test('C is a friend of A (2)', accountC, accountA, false, 'accepted', 1); + + -- friendship request from user B to A + PERFORM vibetype_test.friendship_request(accountB, accountC, 'de'); + + BEGIN + PERFORM vibetype_test.friendship_toggle_closeness(accountB, accountC); + RAISE 'It was possible to toggle closeness in a friendship request.'; + EXCEPTION + WHEN OTHERS THEN + IF SQLSTATE != 'VTFTC' THEN + RAISE; + END IF; + END; + + -- B marks A as a close friend + PERFORM vibetype_test.friendship_toggle_closeness(accountB, accountA); + PERFORM vibetype_test.friendship_test('B marks A as a close friend (1)', accountB, accountA, true, 'accepted', 1); + PERFORM vibetype_test.friendship_test('B marks A as a close friend (2)', accountA, accountB, true, NULL, 0); + END $$; ROLLBACK; diff --git a/test/logic/utility/model/account_registration.sql b/test/logic/utility/model/account_registration.sql index 19d943f9..5741a410 100644 --- a/test/logic/utility/model/account_registration.sql +++ b/test/logic/utility/model/account_registration.sql @@ -8,7 +8,7 @@ DECLARE _verification UUID; BEGIN _legal_term_id := vibetype_test.legal_term_select_by_singleton(); - PERFORM vibetype.account_registration('1970-01-01', _email_address, 'en', _legal_term_id, 'password', _username); + PERFORM vibetype.account_registration(to_date('1970-01-01', 'yyyy-mm-dd'), _email_address, 'en', _legal_term_id, 'password', _username); SELECT id INTO _account_id FROM vibetype.account diff --git a/test/logic/utility/model/friendship.sql b/test/logic/utility/model/friendship.sql index 78337634..c406ae6d 100644 --- a/test/logic/utility/model/friendship.sql +++ b/test/logic/utility/model/friendship.sql @@ -1,6 +1,6 @@ CREATE OR REPLACE FUNCTION vibetype_test.friendship_accept ( _invoker_account_id UUID, - _id UUID + _requestor_account_id UUID ) RETURNS VOID AS $$ DECLARE rec RECORD; @@ -9,9 +9,7 @@ BEGIN SET LOCAL role = 'vibetype_account'; EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || _invoker_account_id || ''''; - UPDATE vibetype.friendship - SET "status" = 'accepted'::vibetype.friendship_status - WHERE id = _id; + PERFORM vibetype.friendship_accept(_requestor_account_id); SET LOCAL ROLE NONE; END $$ LANGUAGE plpgsql; @@ -19,149 +17,80 @@ END $$ LANGUAGE plpgsql; GRANT EXECUTE ON FUNCTION vibetype_test.friendship_accept(UUID, UUID) TO vibetype_account; -CREATE OR REPLACE FUNCTION vibetype_test.friendship_reject ( +CREATE OR REPLACE FUNCTION vibetype_test.friendship_cancel ( _invoker_account_id UUID, - _id UUID + _friend_account_id UUID ) RETURNS VOID AS $$ BEGIN SET LOCAL role = 'vibetype_account'; EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || _invoker_account_id || ''''; - DELETE FROM vibetype.friendship - WHERE id = _id; + PERFORM vibetype.friendship_cancel(_friend_account_id); SET LOCAL ROLE NONE; END $$ LANGUAGE plpgsql; -GRANT EXECUTE ON FUNCTION vibetype_test.friendship_reject(UUID, UUID) TO vibetype_account; +GRANT EXECUTE ON FUNCTION vibetype_test.friendship_cancel(UUID, UUID) TO vibetype_account; CREATE OR REPLACE FUNCTION vibetype_test.friendship_request ( _invoker_account_id UUID, - _friend_account_id UUID -) RETURNS UUID AS $$ -DECLARE - _id UUID; - _a_account_id UUID; - _b_account_id UUID; + _friend_account_id UUID, + _language TEXT +) RETURNS VOID AS $$ BEGIN SET LOCAL role = 'vibetype_account'; EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || _invoker_account_id || ''''; - IF _invoker_account_id < _friend_account_id THEN - _a_account_id := _invoker_account_id; - _b_account_id := _friend_account_id; - ELSE - _a_account_id := _friend_account_id; - _b_account_id := _invoker_account_id; - END IF; - - INSERT INTO vibetype.friendship(a_account_id, b_account_id, created_by) - VALUES (_a_account_id, _b_account_id, _invoker_account_id) - RETURNING id INTO _id; + PERFORM vibetype.friendship_request(_friend_account_id, _language); SET LOCAL ROLE NONE; - - RETURN _id; END $$ LANGUAGE plpgsql; -GRANT EXECUTE ON FUNCTION vibetype_test.friendship_request(UUID, UUID) TO vibetype_account; +GRANT EXECUTE ON FUNCTION vibetype_test.friendship_request(UUID, UUID, TEXT) TO vibetype_account; - -CREATE OR REPLACE FUNCTION vibetype_test.friendship_test ( - _test_case TEXT, +CREATE OR REPLACE FUNCTION vibetype_test.friendship_toggle_closeness ( _invoker_account_id UUID, - _status TEXT, -- status IS NULL means "any status" - _expected_result UUID[] + _friend_account_id UUID ) RETURNS VOID AS $$ -DECLARE - rec RECORD; BEGIN - IF _invoker_account_id IS NULL THEN - SET LOCAL role = 'vibetype_anonymous'; - SET LOCAL jwt.claims.account_id = ''; - ELSE - SET LOCAL role = 'vibetype_account'; - EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || _invoker_account_id || ''''; - END IF; + SET LOCAL role = 'vibetype_account'; + EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || _invoker_account_id || ''''; - IF EXISTS ( - SELECT id FROM vibetype.friendship WHERE _status IS NULL OR status = _status::vibetype.friendship_status - EXCEPT - SELECT * FROM unnest(_expected_result) - ) THEN - RAISE EXCEPTION 'some accounts should not appear in the query result'; - END IF; - - IF EXISTS ( - SELECT * FROM unnest(_expected_result) - EXCEPT - SELECT id FROM vibetype.friendship WHERE _status IS NULL OR status = _status::vibetype.friendship_status - ) THEN - RAISE EXCEPTION 'some account is missing in the query result'; - END IF; + PERFORM vibetype.friendship_toggle_closeness(_friend_account_id); SET LOCAL ROLE NONE; END $$ LANGUAGE plpgsql; -GRANT EXECUTE ON FUNCTION vibetype_test.friendship_test(TEXT, UUID, TEXT, UUID[]) TO vibetype_account; +GRANT EXECUTE ON FUNCTION vibetype_test.friendship_toggle_closeness(UUID, UUID) TO vibetype_account; -CREATE OR REPLACE FUNCTION vibetype_test.friendship_account_ids_test ( +CREATE OR REPLACE FUNCTION vibetype_test.friendship_test ( _test_case TEXT, _invoker_account_id UUID, - _expected_result UUID[] + _friend_account_id UUID, + _is_close_friend BOOLEAN, -- _is_close_friend IS NULL means "any boolean value" + _status TEXT, -- _status IS NULL means "any status" + _expected_count INTEGER ) RETURNS VOID AS $$ DECLARE - rec RECORD; + _result INTEGER; BEGIN - IF _invoker_account_id IS NULL THEN - SET LOCAL jwt.claims.account_id = ''; - ELSE - EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || _invoker_account_id || ''''; - END IF; + SET LOCAL role = 'vibetype_account'; + EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || _invoker_account_id || ''''; - IF EXISTS ( - WITH friendship_account_ids_test AS ( - SELECT b_account_id as account_id - FROM vibetype.friendship - WHERE a_account_id = _invoker_account_id - and status = 'accepted'::vibetype.friendship_status - UNION ALL - SELECT a_account_id as account_id - FROM vibetype.friendship - WHERE b_account_id = _invoker_account_id - and status = 'accepted'::vibetype.friendship_status - ) - SELECT account_id as id - FROM friendship_account_ids_test - WHERE account_id NOT IN (SELECT b.id FROM vibetype_private.account_block_ids() b) - EXCEPT - SELECT * FROM unnest(_expected_result) - ) THEN - RAISE EXCEPTION 'some accounts should not appear in the list of friends'; - END IF; + SELECT count(*) INTO _result + FROM vibetype.friendship + WHERE account_id = _invoker_account_id + AND (_status IS NULL OR status = _status::vibetype.friendship_status) + AND (_is_close_friend IS NULL OR is_close_friend = _is_close_friend) + AND (_friend_account_id IS NULL OR friend_account_id = _friend_account_id); - IF EXISTS ( - WITH friendship_account_ids_test AS ( - SELECT b_account_id as account_id - FROM vibetype.friendship - WHERE a_account_id = vibetype.invoker_account_id() - and status = 'accepted'::vibetype.friendship_status - UNION ALL - SELECT a_account_id as account_id - FROM vibetype.friendship - WHERE b_account_id = vibetype.invoker_account_id() - and status = 'accepted'::vibetype.friendship_status - ) - SELECT * FROM unnest(_expected_result) - EXCEPT - SELECT account_id as id - FROM friendship_account_ids_test - WHERE account_id NOT IN (SELECT b.id FROM vibetype_private.account_block_ids() b) - ) THEN - RAISE EXCEPTION 'some account is missing in the list of friends'; + IF _result != _expected_count THEN + RAISE EXCEPTION 'Expected count was % but result is %.', _expected_count, _result USING ERRCODE = 'VTTST'; END IF; -END $$ LANGUAGE plpgsql SECURITY DEFINER; -GRANT EXECUTE ON FUNCTION vibetype_test.friendship_account_ids_test(TEXT, UUID, UUID[]) TO vibetype_account; + SET LOCAL ROLE NONE; +END $$ LANGUAGE plpgsql; + +GRANT EXECUTE ON FUNCTION vibetype_test.friendship_test(TEXT, UUID, UUID, BOOLEAN, TEXT, INTEGER) TO vibetype_account; From cbbe7aa59e0b3ddc34d494cb428498b6ec529676 Mon Sep 17 00:00:00 2001 From: Sven Thelemann Date: Thu, 7 Aug 2025 18:49:50 +0200 Subject: [PATCH 5/8] feat(friendship): new table friendship_request There is a new table `friendship_request` holding a single row per friendship request. If the request is accepted then 2 records will be inserted into table `friendship` and the row in `friendship_request` will be deleted. If the request is rejected then the row in `friendship_request`will also be deleted deleted. This makes the enum type `friendship_status` superfluous and also allows policies on table `friendship` in order to hide information about friendship closeness as set by other users. --- src/deploy/enum_friendship_status.sql | 11 - src/deploy/function_friendship.sql | 86 ++++--- src/deploy/table_friendship.sql | 98 +++++--- src/revert/enum_friendship_status.sql | 5 - src/revert/function_friendship.sql | 1 + src/revert/table_friendship.sql | 14 +- src/sqitch.plan | 3 +- src/verify/enum_friendship_status.sql | 8 - src/verify/function_friendship.sql | 4 + src/verify/table_friendship.sql | 24 +- test/fixture/schema_vibetype.definition.sql | 248 +++++++++++++++----- test/logic/scenario/model/friendship.sql | 142 +++++++---- test/logic/utility/model/friendship.sql | 59 ++++- 13 files changed, 506 insertions(+), 197 deletions(-) delete mode 100644 src/deploy/enum_friendship_status.sql delete mode 100644 src/revert/enum_friendship_status.sql delete mode 100644 src/verify/enum_friendship_status.sql diff --git a/src/deploy/enum_friendship_status.sql b/src/deploy/enum_friendship_status.sql deleted file mode 100644 index fa585a6c..00000000 --- a/src/deploy/enum_friendship_status.sql +++ /dev/null @@ -1,11 +0,0 @@ -BEGIN; - -CREATE TYPE vibetype.friendship_status AS ENUM ( - 'accepted', - 'requested' -); - -COMMENT ON TYPE vibetype.friendship_status IS 'Possible status values of a friend relation. -There is no status `rejected` because friendship records will be deleted when a friendship request is rejected.'; - -COMMIT; diff --git a/src/deploy/function_friendship.sql b/src/deploy/function_friendship.sql index 14d7137d..1539be43 100644 --- a/src/deploy/function_friendship.sql +++ b/src/deploy/function_friendship.sql @@ -2,39 +2,42 @@ BEGIN; -- accept friendship request -CREATE OR REPLACE FUNCTION vibetype.friendship_accept( +CREATE FUNCTION vibetype.friendship_accept( requestor_account_id UUID ) RETURNS VOID AS $$ DECLARE _friend_account_id UUID; - _count INTEGER; + _id UUID; BEGIN _friend_account_id := vibetype.invoker_account_id(); - UPDATE vibetype.friendship SET - status = 'accepted'::vibetype.friendship_status - -- updated_by filled by trigger - WHERE account_id = requestor_account_id AND friend_account_id = _friend_account_id - AND status = 'requested'::vibetype.friendship_status; + SELECT id INTO _id + FROM vibetype.friendship_request + WHERE account_id = requestor_account_id AND friend_account_id = _friend_account_id; - GET DIAGNOSTICS _count = ROW_COUNT; - IF _count = 0 THEN + IF _id IS NULL THEN RAISE EXCEPTION 'Friendship request does not exist' USING ERRCODE = 'VTFAC'; END IF; - INSERT INTO vibetype.friendship(account_id, friend_account_id, status, created_by) - VALUES (_friend_account_id, requestor_account_id, 'accepted'::vibetype.friendship_status, _friend_account_id); + INSERT INTO vibetype.friendship(account_id, friend_account_id, created_by) + VALUES (requestor_account_id, _friend_account_id, requestor_account_id); + + INSERT INTO vibetype.friendship(account_id, friend_account_id, created_by) + VALUES (_friend_account_id, requestor_account_id, _friend_account_id); + + DELETE FROM vibetype.friendship_request + WHERE account_id = requestor_account_id AND friend_account_id = vibetype.invoker_account_id(); END; $$ LANGUAGE plpgsql SECURITY INVOKER; -COMMENT ON FUNCTION vibetype.friendship_accept(UUID) IS 'Accepts a friendship request.\n\nError codes:\n- **VTFAC** when a corresponding friendship request does not exist.'; +COMMENT ON FUNCTION vibetype.friendship_accept(UUID) IS E'Accepts a friendship request.\n\nError codes:\n- **VTFAC** when a corresponding friendship request does not exist.'; GRANT EXECUTE ON FUNCTION vibetype.friendship_accept(UUID) TO vibetype_account; --- reject or cancel friendship +-- cancel friendship -CREATE OR REPLACE FUNCTION vibetype.friendship_cancel( +CREATE FUNCTION vibetype.friendship_cancel( friend_account_id UUID ) RETURNS VOID AS $$ DECLARE @@ -49,13 +52,13 @@ BEGIN END; $$ LANGUAGE plpgsql SECURITY INVOKER; -COMMENT ON FUNCTION vibetype.friendship_cancel(UUID) IS 'Rejects or cancels a friendship (in both directions).'; +COMMENT ON FUNCTION vibetype.friendship_cancel(UUID) IS 'Cancels a friendship (in both directions) if it exists.'; GRANT EXECUTE ON FUNCTION vibetype.friendship_cancel(UUID) TO vibetype_account; -- create notification for a request -CREATE OR REPLACE FUNCTION vibetype.friendship_notify_request( +CREATE FUNCTION vibetype.friendship_notify_request( friend_account_id UUID, language TEXT ) RETURNS VOID AS $$ @@ -79,9 +82,25 @@ COMMENT ON FUNCTION vibetype.friendship_notify_request(UUID, TEXT) IS 'Creates a GRANT EXECUTE ON FUNCTION vibetype.friendship_notify_request(UUID, TEXT) TO vibetype_account; +-- reject friendship request + +CREATE FUNCTION vibetype.friendship_reject( + requestor_account_id UUID +) RETURNS VOID AS $$ +BEGIN + + DELETE FROM vibetype.friendship_request + WHERE account_id = requestor_account_id AND friend_account_id = vibetype.invoker_account_id(); + +END; $$ LANGUAGE plpgsql SECURITY DEFINER; + +COMMENT ON FUNCTION vibetype.friendship_reject(UUID) IS 'Rejects a friendship request'; + +GRANT EXECUTE ON FUNCTION vibetype.friendship_reject(UUID) TO vibetype_account; + -- request friendship -CREATE OR REPLACE FUNCTION vibetype.friendship_request( +CREATE FUNCTION vibetype.friendship_request( friend_account_id UUID, language TEXT ) RETURNS VOID AS $$ @@ -95,42 +114,53 @@ BEGIN SELECT 1 FROM vibetype.friendship f WHERE (f.account_id = _account_id AND f.friend_account_id = friendship_request.friend_account_id) - OR (f.account_id = friendship_request.friend_account_id AND f.friend_account_id = _account_id) ) THEN - RAISE EXCEPTION 'Friendship already exists or has already been requested.' USING ERRCODE = 'VTREQ'; + RAISE EXCEPTION 'Friendship already exists.' USING ERRCODE = 'VTFEX'; + END IF; + + IF EXISTS( + SELECT 1 + FROM vibetype.friendship_request r + WHERE (r.account_id = _account_id AND r.friend_account_id = friendship_request.friend_account_id) + OR (r.account_id = friendship_request.friend_account_id AND r.friend_account_id = _account_id) + ) + THEN + RAISE EXCEPTION 'There is already a friendship request.' USING ERRCODE = 'VTREQ'; END IF; - INSERT INTO vibetype.friendship(account_id, friend_account_id, status, created_by) - VALUES (_account_id, friendship_request.friend_account_id, 'requested'::vibetype.friendship_status, _account_id); + INSERT INTO vibetype.friendship_request(account_id, friend_account_id, created_by) + VALUES (_account_id, friendship_request.friend_account_id, _account_id); PERFORM vibetype.friendship_notify_request(friendship_request.friend_account_id, friendship_request.language); END; $$ LANGUAGE plpgsql SECURITY INVOKER; -COMMENT ON FUNCTION vibetype.friendship_request(UUID, TEXT) IS 'Starts a new friendship request.\n\nError codes:\n- **VTREQ** when the friendship already exists or has already been requested.'; +COMMENT ON FUNCTION vibetype.friendship_request(UUID, TEXT) IS E'Starts a new friendship request.\n\nError codes:\n- **VTFEX** when the friendship already exists.\n- **VTREQ** when there is already a friendship request.'; GRANT EXECUTE ON FUNCTION vibetype.friendship_request(UUID, TEXT) TO vibetype_account; -- toggle closeness of friendship -CREATE OR REPLACE FUNCTION vibetype.friendship_toggle_closeness( +CREATE FUNCTION vibetype.friendship_toggle_closeness( friend_account_id UUID ) RETURNS BOOLEAN AS $$ DECLARE _account_id UUID; + _id UUID; _is_close_friend BOOLEAN; - current_status vibetype.friendship_status; BEGIN _account_id := vibetype.invoker_account_id(); - SELECT status INTO current_status + SELECT f.id + INTO _id FROM vibetype.friendship f - WHERE f.account_id = _account_id AND f.friend_account_id = friendship_toggle_closeness.friend_account_id; + WHERE f.account_id = _account_id + AND f.friend_account_id = friendship_toggle_closeness.friend_account_id; - IF current_status IS NULL OR current_status != 'accepted'::vibetype.friendship_status THEN + IF _id IS NULL THEN RAISE EXCEPTION 'Friendship does not exist' USING ERRCODE = 'VTFTC'; END IF; @@ -144,7 +174,7 @@ BEGIN END; $$ LANGUAGE plpgsql SECURITY INVOKER; -COMMENT ON FUNCTION vibetype.friendship_toggle_closeness(UUID) IS 'Toggles a frien1dship relation between ''not a close friend'' and ''close friend''.\n\nError codes:\n- **VTFTC** when the friendship does not exist.'; +COMMENT ON FUNCTION vibetype.friendship_toggle_closeness(UUID) IS E'Toggles a friendship relation between ''not a close friend'' and ''close friend''.\n\nError codes:\n- **VTFTC** when the friendship does not exist.'; GRANT EXECUTE ON FUNCTION vibetype.friendship_toggle_closeness(UUID) TO vibetype_account; diff --git a/src/deploy/table_friendship.sql b/src/deploy/table_friendship.sql index 2553f04c..eb6f47f4 100644 --- a/src/deploy/table_friendship.sql +++ b/src/deploy/table_friendship.sql @@ -1,11 +1,59 @@ +----------------------------------------------------------- +-- TABLE vibetype.friendship_request +----------------------------------------------------------- + +CREATE TABLE vibetype.friendship_request ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + account_id UUID NOT NULL REFERENCES vibetype.account(id) ON DELETE CASCADE, + friend_account_id UUID NOT NULL REFERENCES vibetype.account(id) ON DELETE CASCADE, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_by UUID NOT NULL REFERENCES vibetype.account(id) ON DELETE CASCADE, + + UNIQUE (account_id, friend_account_id), + CONSTRAINT friendship_creator_friend CHECK (account_id <> friend_account_id), + CONSTRAINT friendship_creator_participant CHECK (created_by = account_id) +); + +GRANT SELECT, INSERT, DELETE ON TABLE vibetype.friendship_request TO vibetype_account; + +ALTER TABLE vibetype.friendship_request ENABLE ROW LEVEL SECURITY; + +CREATE POLICY friendship_request_not_blocked ON vibetype.friendship_request FOR ALL +USING ( + account_id NOT IN (SELECT id FROM vibetype_private.account_block_ids()) + AND friend_account_id NOT IN (SELECT id FROM vibetype_private.account_block_ids()) +); + +-- Only allow interactions with friendships in which the current user is involved. +CREATE POLICY friendship_request_select ON vibetype.friendship_request FOR SELECT +USING ( + account_id = vibetype.invoker_account_id() + OR + friend_account_id = vibetype.invoker_account_id() +); + +-- Only allow creation by the current user. +CREATE POLICY friendship_request_insert ON vibetype.friendship_request FOR INSERT +WITH CHECK ( + created_by = vibetype.invoker_account_id() +); + +CREATE POLICY friendship_request_delete ON vibetype.friendship_request FOR DELETE +USING ( + friend_account_id = vibetype.invoker_account_id() +); + +----------------------------------------------------------- +-- TABLE vibetype.friendship +----------------------------------------------------------- + CREATE TABLE vibetype.friendship ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), account_id UUID NOT NULL REFERENCES vibetype.account(id) ON DELETE CASCADE, friend_account_id UUID NOT NULL REFERENCES vibetype.account(id) ON DELETE CASCADE, - is_close_friend BOOLEAN NOT NULL DEFAULT false, - status vibetype.friendship_status NOT NULL DEFAULT 'requested'::vibetype.friendship_status, + is_close_friend BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID NOT NULL REFERENCES vibetype.account(id) ON DELETE CASCADE, @@ -25,7 +73,6 @@ COMMENT ON COLUMN vibetype.friendship.id IS E'@omit create,update\nThe friend re COMMENT ON COLUMN vibetype.friendship.account_id IS E'@omit update\nThe one side of the friend relation. If the status is ''requested'' then it is the requestor account.'; COMMENT ON COLUMN vibetype.friendship.friend_account_id IS E'@omit update\nThe other side of the friend relation. If the status is ''requested'' then it is the requestee account.'; COMMENT ON COLUMN vibetype.friendship.is_close_friend IS E'@omit create\nThe flag indicating whether account_id considers friend_account_id as a close friend or not.'; -COMMENT ON COLUMN vibetype.friendship.status IS E'@omit create\nThe status of the friend relation.'; COMMENT ON COLUMN vibetype.friendship.created_at IS E'@omit create,update\nThe timestamp when the friend relation was created.'; COMMENT ON COLUMN vibetype.friendship.created_by IS E'@omit update\nThe account that created the friend relation was created.'; COMMENT ON COLUMN vibetype.friendship.updated_at IS E'@omit create,update\nThe timestamp when the friend relation''s status was updated.'; @@ -40,11 +87,11 @@ CREATE TRIGGER vibetype_trigger_friendship_update FOR EACH ROW EXECUTE PROCEDURE vibetype.trigger_metadata_update(); -GRANT INSERT, SELECT, UPDATE, DELETE ON TABLE vibetype.friendship TO vibetype_account; +GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE vibetype.friendship TO vibetype_account; ALTER TABLE vibetype.friendship ENABLE ROW LEVEL SECURITY; -CREATE POLICY friendship_not_blocked ON vibetype.friendship FOR ALL +CREATE POLICY friendship_not_blocked ON vibetype.friendship AS RESTRICTIVE FOR ALL USING ( account_id NOT IN (SELECT id FROM vibetype_private.account_block_ids()) AND friend_account_id NOT IN (SELECT id FROM vibetype_private.account_block_ids()) @@ -54,41 +101,36 @@ USING ( CREATE POLICY friendship_select ON vibetype.friendship FOR SELECT USING ( account_id = vibetype.invoker_account_id() - OR - friend_account_id = vibetype.invoker_account_id() ); --- Only allow creation by the current user. +-- Only allow creation by the current user and only if a friendship request is present. CREATE POLICY friendship_insert ON vibetype.friendship FOR INSERT WITH CHECK ( - created_by = vibetype.invoker_account_id() -); - --- Only allow update by the current user if it is about accepting a friendship request. -CREATE POLICY friendship_update_accept ON vibetype.friendship FOR UPDATE -USING ( - friend_account_id = vibetype.invoker_account_id() - AND - status = 'requested'::vibetype.friendship_status -) WITH CHECK ( - status = 'accepted'::vibetype.friendship_status - AND - updated_by = vibetype.invoker_account_id() + (account_id, friend_account_id, created_by) IN ( + SELECT account_id, friend_account_id, account_id + FROM vibetype.friendship_request + WHERE friend_account_id = vibetype.invoker_account_id() + ) + OR + (account_id, friend_account_id, created_by) IN ( + SELECT friend_account_id, account_id, friend_account_id + FROM vibetype.friendship_request + WHERE friend_account_id = vibetype.invoker_account_id() + ) ); --- Only allow update by the current user if it is already an accepted relation. -CREATE POLICY friendship_update_toggle_closeness ON vibetype.friendship FOR UPDATE +-- Only allow update by the current user. +CREATE POLICY friendship_update ON vibetype.friendship FOR UPDATE USING ( - status = 'accepted'::vibetype.friendship_status - AND account_id = vibetype.invoker_account_id() ) WITH CHECK ( - status = 'accepted'::vibetype.friendship_status - AND updated_by = vibetype.invoker_account_id() ); +-- Only allow deletion if the current user is involved in the friendship. CREATE POLICY friendship_delete ON vibetype.friendship FOR DELETE USING ( - TRUE + account_id = vibetype.invoker_account_id() + OR + friend_account_id = vibetype.invoker_account_id() ); diff --git a/src/revert/enum_friendship_status.sql b/src/revert/enum_friendship_status.sql deleted file mode 100644 index 04e22d38..00000000 --- a/src/revert/enum_friendship_status.sql +++ /dev/null @@ -1,5 +0,0 @@ -BEGIN; - -DROP TYPE vibetype.friendship_status; - -COMMIT; diff --git a/src/revert/function_friendship.sql b/src/revert/function_friendship.sql index db7c86a7..b02f5fdc 100644 --- a/src/revert/function_friendship.sql +++ b/src/revert/function_friendship.sql @@ -3,6 +3,7 @@ BEGIN; DROP FUNCTION vibetype.friendship_accept(UUID); DROP FUNCTION vibetype.friendship_cancel(UUID); DROP FUNCTION vibetype.friendship_notify_request(UUID, TEXT); +DROP FUNCTION vibetype.friendship_reject(UUID); DROP FUNCTION vibetype.friendship_request(UUID, TEXT); DROP FUNCTION vibetype.friendship_toggle_closeness(UUID); diff --git a/src/revert/table_friendship.sql b/src/revert/table_friendship.sql index 17413551..d55c1efa 100644 --- a/src/revert/table_friendship.sql +++ b/src/revert/table_friendship.sql @@ -1,10 +1,11 @@ BEGIN; +-- vibetype.friendship + DROP POLICY friendship_not_blocked ON vibetype.friendship; DROP POLICY friendship_select ON vibetype.friendship; DROP POLICY friendship_insert ON vibetype.friendship; -DROP POLICY friendship_update_accept ON vibetype.friendship; -DROP POLICY friendship_update_toggle_closeness ON vibetype.friendship; +DROP POLICY friendship_update ON vibetype.friendship; DROP POLICY friendship_delete ON vibetype.friendship; DROP TRIGGER vibetype_trigger_friendship_update ON vibetype.friendship; @@ -13,4 +14,13 @@ DROP INDEX vibetype.idx_friendship_updated_by; DROP INDEX vibetype.idx_friendship_created_by; DROP TABLE vibetype.friendship; +-- vibetype.friendship_request + +DROP POLICY friendship_request_not_blocked ON vibetype.friendship_request; +DROP POLICY friendship_request_select ON vibetype.friendship_request; +DROP POLICY friendship_request_insert ON vibetype.friendship_request; +DROP POLICY friendship_request_delete ON vibetype.friendship_request; + +DROP TABLE vibetype.friendship_request; + COMMIT; diff --git a/src/sqitch.plan b/src/sqitch.plan index 18ee931b..6cac1bd3 100644 --- a/src/sqitch.plan +++ b/src/sqitch.plan @@ -82,8 +82,7 @@ table_event_favorite [schema_public table_account_public table_event] 1970-01-01 function_guest_create_multiple [schema_public table_guest role_account] 1970-01-01T00:00:00Z Sven Thelemann # Function for inserting multiple guest records. function_event_search [privilege_execute_revoke schema_public enum_language schema_private function_language_iso_full_text_search table_event role_account role_anonymous] 1970-01-01T00:00:00Z Jonas Thelemann # Full-text search on events. table_device [schema_public table_account_public function_trigger_metadata_update] 1970-01-01T00:00:00Z Jonas Thelemann # A device that's assigned to an account. -enum_friendship_status [schema_public] 1970-01-01T00:00:00Z Sven Thelemann # Possible status values of a friend relation. -table_friendship [schema_public enum_friendship_status table_account_public function_trigger_metadata_update] 1970-01-01T00:00:00Z Sven Thelemann # A friend relation together with its status. +table_friendship [schema_public table_account_public function_trigger_metadata_update] 1970-01-01T00:00:00Z Sven Thelemann # A friend relation together with its status. table_event_format [schema_public role_anonymous role_account] 1970-01-01T00:00:00Z Jonas Thelemann # Table for storing event formats. table_event_format_mapping [schema_public table_event table_event_format role_anonymous role_account function_invoker_account_id] 1970-01-01T00:00:00Z Jonas Thelemann # Table for storing event to category (M:N) relationships. table_audit_log [schema_private] 1970-01-01T00:00:00Z Sven Thelemann # Table for storing audit log records. diff --git a/src/verify/enum_friendship_status.sql b/src/verify/enum_friendship_status.sql deleted file mode 100644 index 8bbb77c5..00000000 --- a/src/verify/enum_friendship_status.sql +++ /dev/null @@ -1,8 +0,0 @@ -BEGIN; - -DO $$ -BEGIN - ASSERT (SELECT pg_catalog.has_type_privilege('vibetype.friendship_status', 'USAGE')); -END $$; - -ROLLBACK; diff --git a/src/verify/function_friendship.sql b/src/verify/function_friendship.sql index 5a922070..24c7f462 100644 --- a/src/verify/function_friendship.sql +++ b/src/verify/function_friendship.sql @@ -15,6 +15,10 @@ BEGIN RAISE EXCEPTION 'Test failed: vibetype_account does not have EXECUTE privilege for vibetype.friendship_notify_request(UUID, TEXT).'; END IF; + IF NOT (SELECT pg_catalog.has_function_privilege('vibetype_account', 'vibetype.friendship_reject(UUID)', 'EXECUTE')) THEN + RAISE EXCEPTION 'Test failed: vibetype_account does not have EXECUTE privilege for vibetype.friendship_reject(UUID).'; + END IF; + IF NOT (SELECT pg_catalog.has_function_privilege('vibetype_account', 'vibetype.friendship_request(UUID, TEXT)', 'EXECUTE')) THEN RAISE EXCEPTION 'Test failed: vibetype_account does not have EXECUTE privilege for vibetype.friendship_request(UUID, TEXT).'; END IF; diff --git a/src/verify/table_friendship.sql b/src/verify/table_friendship.sql index b57e3a90..ff53670a 100644 --- a/src/verify/table_friendship.sql +++ b/src/verify/table_friendship.sql @@ -1,11 +1,19 @@ BEGIN; +SELECT + id, + account_id, + friend_account_id, + created_at, + created_by +FROM vibetype.friendship_request +WHERE FALSE; + SELECT id, account_id, friend_account_id, is_close_friend, - status, created_at, created_by, updated_at, @@ -34,6 +42,20 @@ BEGIN ASSERT NOT (SELECT pg_catalog.has_table_privilege(current_setting('role.vibetype_username'), 'vibetype.friendship', 'INSERT')); ASSERT NOT (SELECT pg_catalog.has_table_privilege(current_setting('role.vibetype_username'), 'vibetype.friendship', 'UPDATE')); ASSERT NOT (SELECT pg_catalog.has_table_privilege(current_setting('role.vibetype_username'), 'vibetype.friendship', 'DELETE')); + + ASSERT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.friendship_request', 'SELECT')); + ASSERT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.friendship_request', 'INSERT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.friendship_request', 'UPDATE')); + ASSERT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.friendship_request', 'DELETE')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_anonymous', 'vibetype.friendship_request', 'SELECT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_anonymous', 'vibetype.friendship_request', 'INSERT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_anonymous', 'vibetype.friendship_request', 'UPDATE')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_anonymous', 'vibetype.friendship_request', 'DELETE')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege(current_setting('role.vibetype_username'), 'vibetype.friendship_request', 'SELECT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege(current_setting('role.vibetype_username'), 'vibetype.friendship_request', 'INSERT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege(current_setting('role.vibetype_username'), 'vibetype.friendship_request', 'UPDATE')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege(current_setting('role.vibetype_username'), 'vibetype.friendship_request', 'DELETE')); + END $$; ROLLBACK; diff --git a/test/fixture/schema_vibetype.definition.sql b/test/fixture/schema_vibetype.definition.sql index 7430cf3a..404037e2 100644 --- a/test/fixture/schema_vibetype.definition.sql +++ b/test/fixture/schema_vibetype.definition.sql @@ -136,26 +136,6 @@ ALTER TYPE vibetype.event_visibility OWNER TO ci; COMMENT ON TYPE vibetype.event_visibility IS 'Possible visibilities of events and event groups: public, private and unlisted.'; --- --- Name: friendship_status; Type: TYPE; Schema: vibetype; Owner: ci --- - -CREATE TYPE vibetype.friendship_status AS ENUM ( - 'accepted', - 'requested' -); - - -ALTER TYPE vibetype.friendship_status OWNER TO ci; - --- --- Name: TYPE friendship_status; Type: COMMENT; Schema: vibetype; Owner: ci --- - -COMMENT ON TYPE vibetype.friendship_status IS 'Possible status values of a friend relation. -There is no status `rejected` because friendship records will be deleted when a friendship request is rejected.'; - - -- -- Name: invitation_feedback; Type: TYPE; Schema: vibetype; Owner: ci -- @@ -1319,24 +1299,27 @@ CREATE FUNCTION vibetype.friendship_accept(requestor_account_id uuid) RETURNS vo AS $$ DECLARE _friend_account_id UUID; - _count INTEGER; + _id UUID; BEGIN _friend_account_id := vibetype.invoker_account_id(); - UPDATE vibetype.friendship SET - status = 'accepted'::vibetype.friendship_status - -- updated_by filled by trigger - WHERE account_id = requestor_account_id AND friend_account_id = _friend_account_id - AND status = 'requested'::vibetype.friendship_status; + SELECT id INTO _id + FROM vibetype.friendship_request + WHERE account_id = requestor_account_id AND friend_account_id = _friend_account_id; - GET DIAGNOSTICS _count = ROW_COUNT; - IF _count = 0 THEN + IF _id IS NULL THEN RAISE EXCEPTION 'Friendship request does not exist' USING ERRCODE = 'VTFAC'; END IF; - INSERT INTO vibetype.friendship(account_id, friend_account_id, status, created_by) - VALUES (_friend_account_id, requestor_account_id, 'accepted'::vibetype.friendship_status, _friend_account_id); + INSERT INTO vibetype.friendship(account_id, friend_account_id, created_by) + VALUES (requestor_account_id, _friend_account_id, requestor_account_id); + + INSERT INTO vibetype.friendship(account_id, friend_account_id, created_by) + VALUES (_friend_account_id, requestor_account_id, _friend_account_id); + + DELETE FROM vibetype.friendship_request + WHERE account_id = requestor_account_id AND friend_account_id = vibetype.invoker_account_id(); END; $$; @@ -1347,7 +1330,10 @@ ALTER FUNCTION vibetype.friendship_accept(requestor_account_id uuid) OWNER TO ci -- Name: FUNCTION friendship_accept(requestor_account_id uuid); Type: COMMENT; Schema: vibetype; Owner: ci -- -COMMENT ON FUNCTION vibetype.friendship_accept(requestor_account_id uuid) IS 'Accepts a friendship request.\n\nError codes:\n- **VTFAC** when a corresponding friendship request does not exist.'; +COMMENT ON FUNCTION vibetype.friendship_accept(requestor_account_id uuid) IS 'Accepts a friendship request. + +Error codes: +- **VTFAC** when a corresponding friendship request does not exist.'; -- @@ -1376,7 +1362,7 @@ ALTER FUNCTION vibetype.friendship_cancel(friend_account_id uuid) OWNER TO ci; -- Name: FUNCTION friendship_cancel(friend_account_id uuid); Type: COMMENT; Schema: vibetype; Owner: ci -- -COMMENT ON FUNCTION vibetype.friendship_cancel(friend_account_id uuid) IS 'Rejects or cancels a friendship (in both directions).'; +COMMENT ON FUNCTION vibetype.friendship_cancel(friend_account_id uuid) IS 'Cancels a friendship (in both directions) if it exists.'; -- @@ -1412,6 +1398,30 @@ ALTER FUNCTION vibetype.friendship_notify_request(friend_account_id uuid, langua COMMENT ON FUNCTION vibetype.friendship_notify_request(friend_account_id uuid, language text) IS 'Creates a notification for a friendship_request'; +-- +-- Name: friendship_reject(uuid); Type: FUNCTION; Schema: vibetype; Owner: ci +-- + +CREATE FUNCTION vibetype.friendship_reject(requestor_account_id uuid) RETURNS void + LANGUAGE plpgsql SECURITY DEFINER + AS $$ +BEGIN + + DELETE FROM vibetype.friendship_request + WHERE account_id = requestor_account_id AND friend_account_id = vibetype.invoker_account_id(); + +END; $$; + + +ALTER FUNCTION vibetype.friendship_reject(requestor_account_id uuid) OWNER TO ci; + +-- +-- Name: FUNCTION friendship_reject(requestor_account_id uuid); Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON FUNCTION vibetype.friendship_reject(requestor_account_id uuid) IS 'Rejects a friendship request'; + + -- -- Name: friendship_request(uuid, text); Type: FUNCTION; Schema: vibetype; Owner: ci -- @@ -1429,14 +1439,23 @@ BEGIN SELECT 1 FROM vibetype.friendship f WHERE (f.account_id = _account_id AND f.friend_account_id = friendship_request.friend_account_id) - OR (f.account_id = friendship_request.friend_account_id AND f.friend_account_id = _account_id) ) THEN - RAISE EXCEPTION 'Friendship already exists or has already been requested.' USING ERRCODE = 'VTREQ'; + RAISE EXCEPTION 'Friendship already exists.' USING ERRCODE = 'VTFEX'; + END IF; + + IF EXISTS( + SELECT 1 + FROM vibetype.friendship_request r + WHERE (r.account_id = _account_id AND r.friend_account_id = friendship_request.friend_account_id) + OR (r.account_id = friendship_request.friend_account_id AND r.friend_account_id = _account_id) + ) + THEN + RAISE EXCEPTION 'There is already a friendship request.' USING ERRCODE = 'VTREQ'; END IF; - INSERT INTO vibetype.friendship(account_id, friend_account_id, status, created_by) - VALUES (_account_id, friendship_request.friend_account_id, 'requested'::vibetype.friendship_status, _account_id); + INSERT INTO vibetype.friendship_request(account_id, friend_account_id, created_by) + VALUES (_account_id, friendship_request.friend_account_id, _account_id); PERFORM vibetype.friendship_notify_request(friendship_request.friend_account_id, friendship_request.language); @@ -1449,7 +1468,11 @@ ALTER FUNCTION vibetype.friendship_request(friend_account_id uuid, language text -- Name: FUNCTION friendship_request(friend_account_id uuid, language text); Type: COMMENT; Schema: vibetype; Owner: ci -- -COMMENT ON FUNCTION vibetype.friendship_request(friend_account_id uuid, language text) IS 'Starts a new friendship request.\n\nError codes:\n- **VTREQ** when the friendship already exists or has already been requested.'; +COMMENT ON FUNCTION vibetype.friendship_request(friend_account_id uuid, language text) IS 'Starts a new friendship request. + +Error codes: +- **VTFEX** when the friendship already exists. +- **VTREQ** when there is already a friendship request.'; -- @@ -1461,17 +1484,19 @@ CREATE FUNCTION vibetype.friendship_toggle_closeness(friend_account_id uuid) RET AS $$ DECLARE _account_id UUID; + _id UUID; _is_close_friend BOOLEAN; - current_status vibetype.friendship_status; BEGIN _account_id := vibetype.invoker_account_id(); - SELECT status INTO current_status + SELECT f.id + INTO _id FROM vibetype.friendship f - WHERE f.account_id = _account_id AND f.friend_account_id = friendship_toggle_closeness.friend_account_id; + WHERE f.account_id = _account_id + AND f.friend_account_id = friendship_toggle_closeness.friend_account_id; - IF current_status IS NULL OR current_status != 'accepted'::vibetype.friendship_status THEN + IF _id IS NULL THEN RAISE EXCEPTION 'Friendship does not exist' USING ERRCODE = 'VTFTC'; END IF; @@ -1492,7 +1517,10 @@ ALTER FUNCTION vibetype.friendship_toggle_closeness(friend_account_id uuid) OWNE -- Name: FUNCTION friendship_toggle_closeness(friend_account_id uuid); Type: COMMENT; Schema: vibetype; Owner: ci -- -COMMENT ON FUNCTION vibetype.friendship_toggle_closeness(friend_account_id uuid) IS 'Toggles a frien1dship relation between ''not a close friend'' and ''close friend''.\n\nError codes:\n- **VTFTC** when the friendship does not exist.'; +COMMENT ON FUNCTION vibetype.friendship_toggle_closeness(friend_account_id uuid) IS 'Toggles a friendship relation between ''not a close friend'' and ''close friend''. + +Error codes: +- **VTFTC** when the friendship does not exist.'; -- @@ -3501,7 +3529,6 @@ CREATE TABLE vibetype.friendship ( account_id uuid NOT NULL, friend_account_id uuid NOT NULL, is_close_friend boolean DEFAULT false NOT NULL, - status vibetype.friendship_status DEFAULT 'requested'::vibetype.friendship_status NOT NULL, created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, created_by uuid NOT NULL, updated_at timestamp with time zone, @@ -3552,14 +3579,6 @@ COMMENT ON COLUMN vibetype.friendship.is_close_friend IS '@omit create The flag indicating whether account_id considers friend_account_id as a close friend or not.'; --- --- Name: COLUMN friendship.status; Type: COMMENT; Schema: vibetype; Owner: ci --- - -COMMENT ON COLUMN vibetype.friendship.status IS '@omit create -The status of the friend relation.'; - - -- -- Name: COLUMN friendship.created_at; Type: COMMENT; Schema: vibetype; Owner: ci -- @@ -3592,6 +3611,23 @@ COMMENT ON COLUMN vibetype.friendship.updated_by IS '@omit create,update The account that updated the friend relation''s status.'; +-- +-- Name: friendship_request; Type: TABLE; Schema: vibetype; Owner: ci +-- + +CREATE TABLE vibetype.friendship_request ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + account_id uuid NOT NULL, + friend_account_id uuid NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + created_by uuid NOT NULL, + CONSTRAINT friendship_creator_friend CHECK ((account_id <> friend_account_id)), + CONSTRAINT friendship_creator_participant CHECK ((created_by = account_id)) +); + + +ALTER TABLE vibetype.friendship_request OWNER TO ci; + -- -- Name: guest_flat; Type: VIEW; Schema: vibetype; Owner: ci -- @@ -4811,6 +4847,22 @@ ALTER TABLE ONLY vibetype.friendship ADD CONSTRAINT friendship_pkey PRIMARY KEY (id); +-- +-- Name: friendship_request friendship_request_account_id_friend_account_id_key; Type: CONSTRAINT; Schema: vibetype; Owner: ci +-- + +ALTER TABLE ONLY vibetype.friendship_request + ADD CONSTRAINT friendship_request_account_id_friend_account_id_key UNIQUE (account_id, friend_account_id); + + +-- +-- Name: friendship_request friendship_request_pkey; Type: CONSTRAINT; Schema: vibetype; Owner: ci +-- + +ALTER TABLE ONLY vibetype.friendship_request + ADD CONSTRAINT friendship_request_pkey PRIMARY KEY (id); + + -- -- Name: guest guest_event_id_contact_id_key; Type: CONSTRAINT; Schema: vibetype; Owner: ci -- @@ -5496,6 +5548,30 @@ ALTER TABLE ONLY vibetype.friendship ADD CONSTRAINT friendship_friend_account_id_fkey FOREIGN KEY (friend_account_id) REFERENCES vibetype.account(id) ON DELETE CASCADE; +-- +-- Name: friendship_request friendship_request_account_id_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci +-- + +ALTER TABLE ONLY vibetype.friendship_request + ADD CONSTRAINT friendship_request_account_id_fkey FOREIGN KEY (account_id) REFERENCES vibetype.account(id) ON DELETE CASCADE; + + +-- +-- Name: friendship_request friendship_request_created_by_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci +-- + +ALTER TABLE ONLY vibetype.friendship_request + ADD CONSTRAINT friendship_request_created_by_fkey FOREIGN KEY (created_by) REFERENCES vibetype.account(id) ON DELETE CASCADE; + + +-- +-- Name: friendship_request friendship_request_friend_account_id_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci +-- + +ALTER TABLE ONLY vibetype.friendship_request + ADD CONSTRAINT friendship_request_friend_account_id_fkey FOREIGN KEY (friend_account_id) REFERENCES vibetype.account(id) ON DELETE CASCADE; + + -- -- Name: friendship friendship_updated_by_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci -- @@ -5945,44 +6021,81 @@ ALTER TABLE vibetype.friendship ENABLE ROW LEVEL SECURITY; -- Name: friendship friendship_delete; Type: POLICY; Schema: vibetype; Owner: ci -- -CREATE POLICY friendship_delete ON vibetype.friendship FOR DELETE USING (true); +CREATE POLICY friendship_delete ON vibetype.friendship FOR DELETE USING (((account_id = vibetype.invoker_account_id()) OR (friend_account_id = vibetype.invoker_account_id()))); -- -- Name: friendship friendship_insert; Type: POLICY; Schema: vibetype; Owner: ci -- -CREATE POLICY friendship_insert ON vibetype.friendship FOR INSERT WITH CHECK ((created_by = vibetype.invoker_account_id())); +CREATE POLICY friendship_insert ON vibetype.friendship FOR INSERT WITH CHECK ((((account_id, friend_account_id, created_by) IN ( SELECT friendship_request.account_id, + friendship_request.friend_account_id, + friendship_request.account_id + FROM vibetype.friendship_request + WHERE (friendship_request.friend_account_id = vibetype.invoker_account_id()))) OR ((account_id, friend_account_id, created_by) IN ( SELECT friendship_request.friend_account_id, + friendship_request.account_id, + friendship_request.friend_account_id + FROM vibetype.friendship_request + WHERE (friendship_request.friend_account_id = vibetype.invoker_account_id()))))); -- -- Name: friendship friendship_not_blocked; Type: POLICY; Schema: vibetype; Owner: ci -- -CREATE POLICY friendship_not_blocked ON vibetype.friendship USING (((NOT (account_id IN ( SELECT account_block_ids.id +CREATE POLICY friendship_not_blocked ON vibetype.friendship AS RESTRICTIVE USING (((NOT (account_id IN ( SELECT account_block_ids.id FROM vibetype_private.account_block_ids() account_block_ids(id)))) AND (NOT (friend_account_id IN ( SELECT account_block_ids.id FROM vibetype_private.account_block_ids() account_block_ids(id)))))); -- --- Name: friendship friendship_select; Type: POLICY; Schema: vibetype; Owner: ci +-- Name: friendship_request; Type: ROW SECURITY; Schema: vibetype; Owner: ci -- -CREATE POLICY friendship_select ON vibetype.friendship FOR SELECT USING (((account_id = vibetype.invoker_account_id()) OR (friend_account_id = vibetype.invoker_account_id()))); +ALTER TABLE vibetype.friendship_request ENABLE ROW LEVEL SECURITY; +-- +-- Name: friendship_request friendship_request_delete; Type: POLICY; Schema: vibetype; Owner: ci +-- +CREATE POLICY friendship_request_delete ON vibetype.friendship_request FOR DELETE USING ((friend_account_id = vibetype.invoker_account_id())); + + +-- +-- Name: friendship_request friendship_request_insert; Type: POLICY; Schema: vibetype; Owner: ci +-- + +CREATE POLICY friendship_request_insert ON vibetype.friendship_request FOR INSERT WITH CHECK ((created_by = vibetype.invoker_account_id())); + + +-- +-- Name: friendship_request friendship_request_not_blocked; Type: POLICY; Schema: vibetype; Owner: ci +-- + +CREATE POLICY friendship_request_not_blocked ON vibetype.friendship_request USING (((NOT (account_id IN ( SELECT account_block_ids.id + FROM vibetype_private.account_block_ids() account_block_ids(id)))) AND (NOT (friend_account_id IN ( SELECT account_block_ids.id + FROM vibetype_private.account_block_ids() account_block_ids(id)))))); + + +-- +-- Name: friendship_request friendship_request_select; Type: POLICY; Schema: vibetype; Owner: ci -- --- Name: friendship friendship_update_accept; Type: POLICY; Schema: vibetype; Owner: ci + +CREATE POLICY friendship_request_select ON vibetype.friendship_request FOR SELECT USING (((account_id = vibetype.invoker_account_id()) OR (friend_account_id = vibetype.invoker_account_id()))); + + +-- +-- Name: friendship friendship_select; Type: POLICY; Schema: vibetype; Owner: ci -- -CREATE POLICY friendship_update_accept ON vibetype.friendship FOR UPDATE USING (((friend_account_id = vibetype.invoker_account_id()) AND (status = 'requested'::vibetype.friendship_status))) WITH CHECK (((status = 'accepted'::vibetype.friendship_status) AND (updated_by = vibetype.invoker_account_id()))); +CREATE POLICY friendship_select ON vibetype.friendship FOR SELECT USING ((account_id = vibetype.invoker_account_id())); -- --- Name: friendship friendship_update_toggle_closeness; Type: POLICY; Schema: vibetype; Owner: ci +-- Name: friendship friendship_update; Type: POLICY; Schema: vibetype; Owner: ci -- -CREATE POLICY friendship_update_toggle_closeness ON vibetype.friendship FOR UPDATE USING (((status = 'accepted'::vibetype.friendship_status) AND (account_id = vibetype.invoker_account_id()))) WITH CHECK (((status = 'accepted'::vibetype.friendship_status) AND (updated_by = vibetype.invoker_account_id()))); +CREATE POLICY friendship_update ON vibetype.friendship FOR UPDATE USING ((account_id = vibetype.invoker_account_id())) WITH CHECK ((updated_by = vibetype.invoker_account_id())); -- @@ -6434,6 +6547,14 @@ REVOKE ALL ON FUNCTION vibetype.friendship_notify_request(friend_account_id uuid GRANT ALL ON FUNCTION vibetype.friendship_notify_request(friend_account_id uuid, language text) TO vibetype_account; +-- +-- Name: FUNCTION friendship_reject(requestor_account_id uuid); Type: ACL; Schema: vibetype; Owner: ci +-- + +REVOKE ALL ON FUNCTION vibetype.friendship_reject(requestor_account_id uuid) FROM PUBLIC; +GRANT ALL ON FUNCTION vibetype.friendship_reject(requestor_account_id uuid) TO vibetype_account; + + -- -- Name: FUNCTION friendship_request(friend_account_id uuid, language text); Type: ACL; Schema: vibetype; Owner: ci -- @@ -6807,6 +6928,13 @@ GRANT SELECT,INSERT,DELETE ON TABLE vibetype.event_upload TO vibetype_account; GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE vibetype.friendship TO vibetype_account; +-- +-- Name: TABLE friendship_request; Type: ACL; Schema: vibetype; Owner: ci +-- + +GRANT SELECT,INSERT,DELETE ON TABLE vibetype.friendship_request TO vibetype_account; + + -- -- Name: TABLE guest_flat; Type: ACL; Schema: vibetype; Owner: ci -- diff --git a/test/logic/scenario/model/friendship.sql b/test/logic/scenario/model/friendship.sql index 4ced4c97..6008dc1d 100644 --- a/test/logic/scenario/model/friendship.sql +++ b/test/logic/scenario/model/friendship.sql @@ -1,4 +1,4 @@ -\echo test_friendship.. +\echo test_friendship... BEGIN; @@ -8,51 +8,44 @@ DECLARE accountB UUID; accountC UUID; rec RECORD; + _is_close_friend BOOLEAN; + _invoker_account_id UUID; BEGIN - -- before all + -- create accounts accountA := vibetype_test.account_registration_verified('username-a', 'email+a@example.com'); accountB := vibetype_test.account_registration_verified('username-b', 'email+b@example.com'); accountC := vibetype_test.account_registration_verified('username-c', 'email+c@example.com'); + PERFORM vibetype_test.friendship_request_test('before A sends request to B', accountA, accountB, false); + -- friendship request from user A to B PERFORM vibetype_test.friendship_request(accountA, accountB, 'de'); - - RAISE NOTICE '----'; - FOR rec IN - SELECT a.username, b.username as friend_username, f.is_close_friend, f.status - FROM vibetype.friendship f - JOIN vibetype.account a ON f.account_id = a.id - JOIN vibetype.account b ON f.friend_account_id = b.id - LOOP - RAISE NOTICE 'friendship: account = %, friend_account = %, is_close_friend = %, status = %', rec.username, rec.friend_username, rec.is_close_friend, rec.status; - END LOOP; - - PERFORM vibetype_test.friendship_test('A sends B a friendship request (1)', accountA, accountB, false, 'requested', 1); - PERFORM vibetype_test.friendship_test('A sends B a friendship request (2)', accountB, accountA, false, 'requested', 0); - PERFORM vibetype_test.friendship_test('A sends B a friendship request (3)', accountA, accountB, false, null, 1); - PERFORM vibetype_test.friendship_test('A sends B a friendship request (3)', accountB, accountA, false, null, 0); + PERFORM vibetype_test.friendship_request_test('after A sends request to B (1)', accountA, accountB, true); + PERFORM vibetype_test.friendship_test('after A sends request to B (2)', accountA, accountB, null, 0); + PERFORM vibetype_test.friendship_test('after A sends request to B (3)', accountB, accountA, null, 0); -- B accepts A's friendship request PERFORM vibetype_test.friendship_accept(accountB, accountA); - PERFORM vibetype_test.friendship_test('B accepts friendship request from A (1)', accountA, accountB, false, 'requested', 0); - PERFORM vibetype_test.friendship_test('B accepts friendship request from A (2)', accountA, accountB, false, 'accepted', 1); - PERFORM vibetype_test.friendship_test('B accepts friendship request from A (3)', accountB, accountA, false, 'accepted', 1); + PERFORM vibetype_test.friendship_request_test('B accepts friendship request from A (1)', accountA, accountB, false); + PERFORM vibetype_test.friendship_test('B accepts friendship request from A (2)', accountA, accountB, false, 1); + PERFORM vibetype_test.friendship_test('B accepts friendship request from A (3)', accountB, accountA, false, 1); -- friendship request from user C to A PERFORM vibetype_test.friendship_request(accountC, accountA, 'de'); - PERFORM vibetype_test.friendship_test('There is still only one accepted friendship for user A', accountA, null, false, 'accepted', 1); - PERFORM vibetype_test.friendship_test('There is a new requested friendship for user C (1)', accountC, null, false, 'requested', 1); - PERFORM vibetype_test.friendship_test('There is a new requested friendship for user C (2)', accountA, accountC, false, null, 0); - PERFORM vibetype_test.friendship_test('User B is still a friend of user A (1)', accountA, accountB, false, null, 1); - PERFORM vibetype_test.friendship_test('User B is still a friend of user A (2)', accountB, accountA, false, null, 1); - PERFORM vibetype_test.friendship_test('User C has no friends', accountC, null, false, 'accepted', 0); + PERFORM vibetype_test.friendship_request_test('after C sends request to A (1)', accountC, accountA, true); + PERFORM vibetype_test.friendship_test('after C sends request to A (2)', accountC, accountA, null, 0); + PERFORM vibetype_test.friendship_test('after A sends request to B (3)', accountA, accountC, null, 0); + PERFORM vibetype_test.friendship_test('User B is still a friend of user A (1)', accountA, accountB, null, 1); + PERFORM vibetype_test.friendship_test('User A is still a friend of user B (2)', accountB, accountA, null, 1); + PERFORM vibetype_test.friendship_test('User C has no friends', accountC, null, null, 0); BEGIN + -- C sends another request to A, should lead to exception VTREQ PERFORM vibetype_test.friendship_request(accountA, accountC, 'de'); - RAISE 'It was possible to request a friendship more than once.'; + RAISE 'C sends another request to A: it was possible to request a friendship more than once.'; EXCEPTION WHEN OTHERS THEN IF SQLSTATE != 'VTREQ' THEN @@ -60,35 +53,94 @@ BEGIN END IF; END; - -- friendship rejection - PERFORM vibetype_test.friendship_cancel(accountA, accountC); - PERFORM vibetype_test.friendship_test('After user A rejected user C''s friendship request (1)', accountC, accountA, false, null, 0); - PERFORM vibetype_test.friendship_test('After user A rejected user C''s friendship request (2)', accountA, accountC, false, null, 0); - - -- a new friendship request from user C to A, this time accepted by A - PERFORM vibetype_test.friendship_request(accountC, accountA, 'de'); - PERFORM vibetype_test.friendship_accept(accountA, accountC); - PERFORM vibetype_test.friendship_test('Count the number of A''s friends', accountA, null, false, 'accepted', 2); - PERFORM vibetype_test.friendship_test('C is a friend of A (1)', accountA, accountC, false, 'accepted', 1); - PERFORM vibetype_test.friendship_test('C is a friend of A (2)', accountC, accountA, false, 'accepted', 1); + BEGIN + -- A sends a request to C, should lead to exception VTREQ + PERFORM vibetype_test.friendship_request(accountA, accountC, 'de'); + RAISE 'A sends a request to C: it was possible to request a friendship more than once.'; + EXCEPTION + WHEN OTHERS THEN + IF SQLSTATE != 'VTREQ' THEN + RAISE; + END IF; + END; - -- friendship request from user B to A - PERFORM vibetype_test.friendship_request(accountB, accountC, 'de'); + BEGIN + -- A sends a new request to B, should lead to exception VTFEX + PERFORM vibetype_test.friendship_request(accountA, accountB, 'de'); + RAISE 'A sends a new request to B: it was possible to request for an already existing friendship.'; + EXCEPTION + WHEN OTHERS THEN + IF SQLSTATE != 'VTFEX' THEN + RAISE; + END IF; + END; BEGIN - PERFORM vibetype_test.friendship_toggle_closeness(accountB, accountC); - RAISE 'It was possible to toggle closeness in a friendship request.'; + -- B sends a new request to A, should lead to exception VTFEX + PERFORM vibetype_test.friendship_request(accountB, accountA, 'de'); + RAISE 'It was possible to request for an already existing friendship.'; EXCEPTION WHEN OTHERS THEN - IF SQLSTATE != 'VTFTC' THEN + IF SQLSTATE != 'VTFEX' THEN RAISE; END IF; END; + -- A rejects friendship request from C + PERFORM vibetype_test.friendship_reject(accountA, accountC); + PERFORM vibetype_test.friendship_test('After A rejected C''s friendship request (1)', accountC, accountA, null, 0); + PERFORM vibetype_test.friendship_test('After A rejected C''s friendship request (2)', accountA, accountC, null, 0); + + -- a new friendship request from user C to A, this time accepted by A + PERFORM vibetype_test.friendship_request(accountC, accountA, 'de'); + PERFORM vibetype_test.friendship_accept(accountA, accountC); + PERFORM vibetype_test.friendship_test('Count the number of A''s friends', accountA, null, null, 2); + PERFORM vibetype_test.friendship_test('C is a friend of A (1)', accountA, accountC, null, 1); + PERFORM vibetype_test.friendship_test('C is a friend of A (2)', accountC, accountA, null, 1); + + -- friendship request from user B to A + PERFORM vibetype_test.friendship_request(accountB, accountC, 'de'); + + -- B marks A as a close friend PERFORM vibetype_test.friendship_toggle_closeness(accountB, accountA); - PERFORM vibetype_test.friendship_test('B marks A as a close friend (1)', accountB, accountA, true, 'accepted', 1); - PERFORM vibetype_test.friendship_test('B marks A as a close friend (2)', accountA, accountB, true, NULL, 0); + +/* + RAISE NOTICE '----'; + FOR rec IN + SELECT a.username, b.username as friend_username, f.is_close_friend + FROM vibetype.friendship f + JOIN vibetype.account a ON f.account_id = a.id + JOIN vibetype.account b ON f.friend_account_id = b.id + LOOP + RAISE NOTICE 'friendship: account = %, friend_account = %, is_close_friend = %', rec.username, rec.friend_username, rec.is_close_friend; + END LOOP; +*/ + + PERFORM vibetype_test.friendship_test('B marks A as a close friend (1)', accountB, accountA, true, 1); + PERFORM vibetype_test.friendship_test('B marks A as a close friend (2)', accountA, accountB, false, 1); + + -- B unmarks A as a close friend + PERFORM vibetype_test.friendship_toggle_closeness(accountB, accountA); + PERFORM vibetype_test.friendship_test('B marks A as a close friend (1)', accountB, accountA, false, 1); + + -- C marks A as a close friend + PERFORM vibetype_test.friendship_toggle_closeness(accountC, accountA); + + -- A wants to find out, if A is a close friend of C. The result should be NULL. + + SET LOCAL role = 'vibetype_account'; + EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || accountA || ''''; + + SELECT is_close_friend INTO _is_close_friend + FROM vibetype.friendship + WHERE account_id = accountC and friend_account_id = accountA; + + IF _is_close_friend IS NOT NULL THEN + RAISE EXCEPTION 'Closeness should not be disclosed to A.'; + END IF; + + SET LOCAL ROLE NONE; END $$; diff --git a/test/logic/utility/model/friendship.sql b/test/logic/utility/model/friendship.sql index c406ae6d..56962275 100644 --- a/test/logic/utility/model/friendship.sql +++ b/test/logic/utility/model/friendship.sql @@ -33,6 +33,22 @@ END $$ LANGUAGE plpgsql; GRANT EXECUTE ON FUNCTION vibetype_test.friendship_cancel(UUID, UUID) TO vibetype_account; +CREATE OR REPLACE FUNCTION vibetype_test.friendship_reject ( + _invoker_account_id UUID, + _friend_account_id UUID +) RETURNS VOID AS $$ +BEGIN + SET LOCAL role = 'vibetype_account'; + EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || _invoker_account_id || ''''; + + PERFORM vibetype.friendship_reject(_friend_account_id); + + SET LOCAL ROLE NONE; +END $$ LANGUAGE plpgsql; + +GRANT EXECUTE ON FUNCTION vibetype_test.friendship_reject(UUID, UUID) TO vibetype_account; + + CREATE OR REPLACE FUNCTION vibetype_test.friendship_request ( _invoker_account_id UUID, _friend_account_id UUID, @@ -68,9 +84,8 @@ GRANT EXECUTE ON FUNCTION vibetype_test.friendship_toggle_closeness(UUID, UUID) CREATE OR REPLACE FUNCTION vibetype_test.friendship_test ( _test_case TEXT, _invoker_account_id UUID, - _friend_account_id UUID, + _friend_account_id UUID, -- _friend_account_id IS NULL means "any friend" _is_close_friend BOOLEAN, -- _is_close_friend IS NULL means "any boolean value" - _status TEXT, -- _status IS NULL means "any status" _expected_count INTEGER ) RETURNS VOID AS $$ DECLARE @@ -82,15 +97,45 @@ BEGIN SELECT count(*) INTO _result FROM vibetype.friendship WHERE account_id = _invoker_account_id - AND (_status IS NULL OR status = _status::vibetype.friendship_status) - AND (_is_close_friend IS NULL OR is_close_friend = _is_close_friend) - AND (_friend_account_id IS NULL OR friend_account_id = _friend_account_id); + AND (_friend_account_id IS NULL OR friend_account_id = _friend_account_id) + AND (_is_close_friend IS NULL OR is_close_friend = _is_close_friend); IF _result != _expected_count THEN - RAISE EXCEPTION 'Expected count was % but result is %.', _expected_count, _result USING ERRCODE = 'VTTST'; + RAISE EXCEPTION '%: expected count was % but result is %.', _test_case, _expected_count, _result USING ERRCODE = 'VTTST'; + END IF; + + SET LOCAL ROLE NONE; +END $$ LANGUAGE plpgsql; + +GRANT EXECUTE ON FUNCTION vibetype_test.friendship_test(TEXT, UUID, UUID, BOOLEAN, INTEGER) TO vibetype_account; + + +CREATE OR REPLACE FUNCTION vibetype_test.friendship_request_test ( + _test_case TEXT, + _invoker_account_id UUID, + _friend_account_id UUID, + _expected_to_exist BOOLEAN +) RETURNS VOID AS $$ +DECLARE + _id UUID; +BEGIN + SET LOCAL role = 'vibetype_account'; + EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || _invoker_account_id || ''''; + + SELECT id INTO _id + FROM vibetype.friendship_request + WHERE account_id = _invoker_account_id + AND friend_account_id = _friend_account_id; + + IF _id IS NULL AND _expected_to_exist THEN + RAISE EXCEPTION '%: friendship request expected to exist but not present.', _test_case USING ERRCODE = 'VTFRT'; + END IF; + + IF _id IS NOT NULL AND NOT _expected_to_exist THEN + RAISE EXCEPTION '%: friendship request exists but is not expected to exist.', _test_case USING ERRCODE = 'VTFRT'; END IF; SET LOCAL ROLE NONE; END $$ LANGUAGE plpgsql; -GRANT EXECUTE ON FUNCTION vibetype_test.friendship_test(TEXT, UUID, UUID, BOOLEAN, TEXT, INTEGER) TO vibetype_account; +GRANT EXECUTE ON FUNCTION vibetype_test.friendship_request_test(TEXT, UUID, UUID, BOOLEAN) TO vibetype_account; From 0b2f7242f7de0e57ab121d3dfc430f10ee3c4a19 Mon Sep 17 00:00:00 2001 From: Sven Thelemann Date: Tue, 12 Aug 2025 19:31:14 +0200 Subject: [PATCH 6/8] feat(friendship): change visibility of friendship relationships If an account A marks account B as a close friend, B can now see this information. An account C cannot select any friendship in which C is not involved. --- src/deploy/table_friendship.sql | 4 +- test/fixture/schema_vibetype.definition.sql | 4 +- test/logic/scenario/model/friendship.sql | 85 ++++++++++++--------- test/logic/utility/model/friendship.sql | 10 ++- 4 files changed, 58 insertions(+), 45 deletions(-) diff --git a/src/deploy/table_friendship.sql b/src/deploy/table_friendship.sql index eb6f47f4..73648a13 100644 --- a/src/deploy/table_friendship.sql +++ b/src/deploy/table_friendship.sql @@ -18,7 +18,7 @@ GRANT SELECT, INSERT, DELETE ON TABLE vibetype.friendship_request TO vibetype_ac ALTER TABLE vibetype.friendship_request ENABLE ROW LEVEL SECURITY; -CREATE POLICY friendship_request_not_blocked ON vibetype.friendship_request FOR ALL +CREATE POLICY friendship_request_not_blocked ON vibetype.friendship_request AS RESTRICTIVE FOR ALL USING ( account_id NOT IN (SELECT id FROM vibetype_private.account_block_ids()) AND friend_account_id NOT IN (SELECT id FROM vibetype_private.account_block_ids()) @@ -101,6 +101,8 @@ USING ( CREATE POLICY friendship_select ON vibetype.friendship FOR SELECT USING ( account_id = vibetype.invoker_account_id() + OR + friend_account_id = vibetype.invoker_account_id() ); -- Only allow creation by the current user and only if a friendship request is present. diff --git a/test/fixture/schema_vibetype.definition.sql b/test/fixture/schema_vibetype.definition.sql index 404037e2..7912fc0b 100644 --- a/test/fixture/schema_vibetype.definition.sql +++ b/test/fixture/schema_vibetype.definition.sql @@ -6072,7 +6072,7 @@ CREATE POLICY friendship_request_insert ON vibetype.friendship_request FOR INSER -- Name: friendship_request friendship_request_not_blocked; Type: POLICY; Schema: vibetype; Owner: ci -- -CREATE POLICY friendship_request_not_blocked ON vibetype.friendship_request USING (((NOT (account_id IN ( SELECT account_block_ids.id +CREATE POLICY friendship_request_not_blocked ON vibetype.friendship_request AS RESTRICTIVE USING (((NOT (account_id IN ( SELECT account_block_ids.id FROM vibetype_private.account_block_ids() account_block_ids(id)))) AND (NOT (friend_account_id IN ( SELECT account_block_ids.id FROM vibetype_private.account_block_ids() account_block_ids(id)))))); @@ -6088,7 +6088,7 @@ CREATE POLICY friendship_request_select ON vibetype.friendship_request FOR SELEC -- Name: friendship friendship_select; Type: POLICY; Schema: vibetype; Owner: ci -- -CREATE POLICY friendship_select ON vibetype.friendship FOR SELECT USING ((account_id = vibetype.invoker_account_id())); +CREATE POLICY friendship_select ON vibetype.friendship FOR SELECT USING (((account_id = vibetype.invoker_account_id()) OR (friend_account_id = vibetype.invoker_account_id()))); -- diff --git a/test/logic/scenario/model/friendship.sql b/test/logic/scenario/model/friendship.sql index 6008dc1d..91536e16 100644 --- a/test/logic/scenario/model/friendship.sql +++ b/test/logic/scenario/model/friendship.sql @@ -16,31 +16,35 @@ BEGIN accountB := vibetype_test.account_registration_verified('username-b', 'email+b@example.com'); accountC := vibetype_test.account_registration_verified('username-c', 'email+c@example.com'); - PERFORM vibetype_test.friendship_request_test('before A sends request to B', accountA, accountB, false); + PERFORM vibetype_test.friendship_request_test('before A sends request to B', accountA, accountA, accountB, false); -- friendship request from user A to B PERFORM vibetype_test.friendship_request(accountA, accountB, 'de'); - PERFORM vibetype_test.friendship_request_test('after A sends request to B (1)', accountA, accountB, true); - PERFORM vibetype_test.friendship_test('after A sends request to B (2)', accountA, accountB, null, 0); - PERFORM vibetype_test.friendship_test('after A sends request to B (3)', accountB, accountA, null, 0); + PERFORM vibetype_test.friendship_request_test('after A sends request to B (1)', accountA, accountA, accountB, true); + PERFORM vibetype_test.friendship_request_test('after A sends request to B (2)', accountB, accountA, accountB, true); + PERFORM vibetype_test.friendship_test('after A sends request to B (3)', accountA, accountA, accountB, null, 0); + PERFORM vibetype_test.friendship_test('after A sends request to B (4)', accountB, accountB, accountA, null, 0); + PERFORM vibetype_test.friendship_request_test('C cannot seen the friendship request from A to B', accountC, accountA, accountB, false); -- B accepts A's friendship request PERFORM vibetype_test.friendship_accept(accountB, accountA); - PERFORM vibetype_test.friendship_request_test('B accepts friendship request from A (1)', accountA, accountB, false); - PERFORM vibetype_test.friendship_test('B accepts friendship request from A (2)', accountA, accountB, false, 1); - PERFORM vibetype_test.friendship_test('B accepts friendship request from A (3)', accountB, accountA, false, 1); + PERFORM vibetype_test.friendship_request_test('B accepts friendship request from A (1)', accountA, accountA, accountB, false); + PERFORM vibetype_test.friendship_test('B accepts friendship request from A (2)', accountA, accountA, accountB, false, 1); + PERFORM vibetype_test.friendship_test('B accepts friendship request from A (3)', accountB, accountB, accountA, false, 1); + PERFORM vibetype_test.friendship_test('C cannot seen the friendship between A and B (1)', accountC, accountA, accountB, null, 0); + PERFORM vibetype_test.friendship_test('C cannot seen the friendship between A and B (2)', accountC, accountB, accountA, null, 0); -- friendship request from user C to A PERFORM vibetype_test.friendship_request(accountC, accountA, 'de'); - PERFORM vibetype_test.friendship_request_test('after C sends request to A (1)', accountC, accountA, true); - PERFORM vibetype_test.friendship_test('after C sends request to A (2)', accountC, accountA, null, 0); - PERFORM vibetype_test.friendship_test('after A sends request to B (3)', accountA, accountC, null, 0); - PERFORM vibetype_test.friendship_test('User B is still a friend of user A (1)', accountA, accountB, null, 1); - PERFORM vibetype_test.friendship_test('User A is still a friend of user B (2)', accountB, accountA, null, 1); - PERFORM vibetype_test.friendship_test('User C has no friends', accountC, null, null, 0); + PERFORM vibetype_test.friendship_request_test('after C sends request to A (1)', accountC, accountC, accountA, true); + PERFORM vibetype_test.friendship_test('after C sends request to A (2)', accountC, accountC, accountA, null, 0); + PERFORM vibetype_test.friendship_test('after A sends request to B (3)', accountA, accountA, accountC, null, 0); + PERFORM vibetype_test.friendship_test('User B is still a friend of user A (1)', accountA, accountA, accountB, null, 1); + PERFORM vibetype_test.friendship_test('User A is still a friend of user B (2)', accountB, accountB, accountA, null, 1); + PERFORM vibetype_test.friendship_test('User C has no friends', accountC, accountC, null, null, 0); BEGIN -- C sends another request to A, should lead to exception VTREQ @@ -88,20 +92,21 @@ BEGIN -- A rejects friendship request from C PERFORM vibetype_test.friendship_reject(accountA, accountC); - PERFORM vibetype_test.friendship_test('After A rejected C''s friendship request (1)', accountC, accountA, null, 0); - PERFORM vibetype_test.friendship_test('After A rejected C''s friendship request (2)', accountA, accountC, null, 0); + PERFORM vibetype_test.friendship_test('After A rejected C''s friendship request (1)', accountA, accountC, accountA, null, 0); + PERFORM vibetype_test.friendship_test('After A rejected C''s friendship request (2)', accountC, accountC, accountA, null, 0); -- a new friendship request from user C to A, this time accepted by A PERFORM vibetype_test.friendship_request(accountC, accountA, 'de'); PERFORM vibetype_test.friendship_accept(accountA, accountC); - PERFORM vibetype_test.friendship_test('Count the number of A''s friends', accountA, null, null, 2); - PERFORM vibetype_test.friendship_test('C is a friend of A (1)', accountA, accountC, null, 1); - PERFORM vibetype_test.friendship_test('C is a friend of A (2)', accountC, accountA, null, 1); + PERFORM vibetype_test.friendship_test('Count the number of A''s friends', accountA, accountA, null, null, 2); + PERFORM vibetype_test.friendship_test('C is a friend of A (1)', accountA, accountA, accountC, null, 1); + PERFORM vibetype_test.friendship_test('C is a friend of A (2)', accountA, accountC, accountA, null, 1); + PERFORM vibetype_test.friendship_test('C is a friend of A (3)', accountC, accountA, accountC, null, 1); + PERFORM vibetype_test.friendship_test('C is a friend of A (4)', accountC, accountC, accountA, null, 1); - -- friendship request from user B to A + -- friendship request from user B to C PERFORM vibetype_test.friendship_request(accountB, accountC, 'de'); - -- B marks A as a close friend PERFORM vibetype_test.friendship_toggle_closeness(accountB, accountA); @@ -117,30 +122,34 @@ BEGIN END LOOP; */ - PERFORM vibetype_test.friendship_test('B marks A as a close friend (1)', accountB, accountA, true, 1); - PERFORM vibetype_test.friendship_test('B marks A as a close friend (2)', accountA, accountB, false, 1); - - -- B unmarks A as a close friend - PERFORM vibetype_test.friendship_toggle_closeness(accountB, accountA); - PERFORM vibetype_test.friendship_test('B marks A as a close friend (1)', accountB, accountA, false, 1); + PERFORM vibetype_test.friendship_test('B marks A as a close friend (1)', accountB, accountB, accountA, true, 1); + PERFORM vibetype_test.friendship_test('B marks A as a close friend (2)', accountB, accountA, accountB, false, 1); + PERFORM vibetype_test.friendship_test('B marks A as a close friend (3)', accountA, accountB, accountA, true, 1); + PERFORM vibetype_test.friendship_test('B marks A as a close friend (4)', accountA, accountA, accountB, false, 1); - -- C marks A as a close friend - PERFORM vibetype_test.friendship_toggle_closeness(accountC, accountA); + -- A marks B as a close friend + PERFORM vibetype_test.friendship_toggle_closeness(accountA, accountB); - -- A wants to find out, if A is a close friend of C. The result should be NULL. + PERFORM vibetype_test.friendship_test('A marks B as a close friend (1)', accountB, accountB, accountA, true, 1); + PERFORM vibetype_test.friendship_test('A marks B as a close friend (2)', accountB, accountA, accountB, true, 1); + PERFORM vibetype_test.friendship_test('A marks B as a close friend (3)', accountA, accountB, accountA, true, 1); + PERFORM vibetype_test.friendship_test('A marks B as a close friend (4)', accountA, accountA, accountB, true, 1); - SET LOCAL role = 'vibetype_account'; - EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || accountA || ''''; + -- B unmarks A as a close friend + PERFORM vibetype_test.friendship_toggle_closeness(accountB, accountA); - SELECT is_close_friend INTO _is_close_friend - FROM vibetype.friendship - WHERE account_id = accountC and friend_account_id = accountA; + PERFORM vibetype_test.friendship_test('B unmarks A as a close friend (1)', accountB, accountB, accountA, false, 1); + PERFORM vibetype_test.friendship_test('B unmarks A as a close friend (2)', accountB, accountA, accountB, true, 1); + PERFORM vibetype_test.friendship_test('B unmarks A as a close friend (3)', accountA, accountB, accountA, false, 1); + PERFORM vibetype_test.friendship_test('B unmarks A as a close friend (4)', accountA, accountA, accountB, true, 1); - IF _is_close_friend IS NOT NULL THEN - RAISE EXCEPTION 'Closeness should not be disclosed to A.'; - END IF; + -- A cancels friendship with C + PERFORM vibetype_test.friendship_cancel(accountA, accountC); - SET LOCAL ROLE NONE; + PERFORM vibetype_test.friendship_test('A cancels friendship with C (1)', accountA, accountC, accountA, null, 0); + PERFORM vibetype_test.friendship_test('A cancels friendship with C (2)', accountA, accountA, accountC, null, 0); + PERFORM vibetype_test.friendship_test('A cancels friendship with C (3)', accountC, accountC, accountA, null, 0); + PERFORM vibetype_test.friendship_test('A cancels friendship with C (4)', accountC, accountA, accountC, null, 0); END $$; diff --git a/test/logic/utility/model/friendship.sql b/test/logic/utility/model/friendship.sql index 56962275..d8d7f233 100644 --- a/test/logic/utility/model/friendship.sql +++ b/test/logic/utility/model/friendship.sql @@ -84,6 +84,7 @@ GRANT EXECUTE ON FUNCTION vibetype_test.friendship_toggle_closeness(UUID, UUID) CREATE OR REPLACE FUNCTION vibetype_test.friendship_test ( _test_case TEXT, _invoker_account_id UUID, + _account_id UUID, _friend_account_id UUID, -- _friend_account_id IS NULL means "any friend" _is_close_friend BOOLEAN, -- _is_close_friend IS NULL means "any boolean value" _expected_count INTEGER @@ -96,7 +97,7 @@ BEGIN SELECT count(*) INTO _result FROM vibetype.friendship - WHERE account_id = _invoker_account_id + WHERE account_id = _account_id AND (_friend_account_id IS NULL OR friend_account_id = _friend_account_id) AND (_is_close_friend IS NULL OR is_close_friend = _is_close_friend); @@ -107,12 +108,13 @@ BEGIN SET LOCAL ROLE NONE; END $$ LANGUAGE plpgsql; -GRANT EXECUTE ON FUNCTION vibetype_test.friendship_test(TEXT, UUID, UUID, BOOLEAN, INTEGER) TO vibetype_account; +GRANT EXECUTE ON FUNCTION vibetype_test.friendship_test(TEXT, UUID, UUID, UUID, BOOLEAN, INTEGER) TO vibetype_account; CREATE OR REPLACE FUNCTION vibetype_test.friendship_request_test ( _test_case TEXT, _invoker_account_id UUID, + _account_id UUID, _friend_account_id UUID, _expected_to_exist BOOLEAN ) RETURNS VOID AS $$ @@ -124,7 +126,7 @@ BEGIN SELECT id INTO _id FROM vibetype.friendship_request - WHERE account_id = _invoker_account_id + WHERE account_id = _account_id AND friend_account_id = _friend_account_id; IF _id IS NULL AND _expected_to_exist THEN @@ -138,4 +140,4 @@ BEGIN SET LOCAL ROLE NONE; END $$ LANGUAGE plpgsql; -GRANT EXECUTE ON FUNCTION vibetype_test.friendship_request_test(TEXT, UUID, UUID, BOOLEAN) TO vibetype_account; +GRANT EXECUTE ON FUNCTION vibetype_test.friendship_request_test(TEXT, UUID, UUID, UUID, BOOLEAN) TO vibetype_account; From 060a85a62ced4851d1c5b8387da7630e395a0272 Mon Sep 17 00:00:00 2001 From: Sven Thelemann Date: Wed, 20 Aug 2025 22:42:14 +0200 Subject: [PATCH 7/8] feat(friendship): rework visibility of friendship and closeness Friendships are visible to any account. Only the account that declared closeness to another account can see this information. --- src/deploy/function_friendship.sql | 16 +- src/deploy/table_friendship.sql | 115 +++++++--- src/revert/table_friendship.sql | 20 +- src/verify/table_friendship.sql | 26 ++- test/fixture/schema_vibetype.definition.sql | 241 ++++++++++++++++---- test/logic/scenario/database/index.sql | 6 +- test/logic/scenario/model/friendship.sql | 78 +++---- test/logic/utility/model/friendship.sql | 47 +++- 8 files changed, 413 insertions(+), 136 deletions(-) diff --git a/src/deploy/function_friendship.sql b/src/deploy/function_friendship.sql index 1539be43..6f417531 100644 --- a/src/deploy/function_friendship.sql +++ b/src/deploy/function_friendship.sql @@ -26,6 +26,12 @@ BEGIN INSERT INTO vibetype.friendship(account_id, friend_account_id, created_by) VALUES (_friend_account_id, requestor_account_id, _friend_account_id); + INSERT INTO vibetype.friendship_closeness(account_id, friend_account_id, created_by) + VALUES (requestor_account_id, _friend_account_id, requestor_account_id); + + INSERT INTO vibetype.friendship_closeness(account_id, friend_account_id, created_by) + VALUES (_friend_account_id, requestor_account_id, _friend_account_id); + DELETE FROM vibetype.friendship_request WHERE account_id = requestor_account_id AND friend_account_id = vibetype.invoker_account_id(); @@ -148,23 +154,23 @@ CREATE FUNCTION vibetype.friendship_toggle_closeness( ) RETURNS BOOLEAN AS $$ DECLARE _account_id UUID; - _id UUID; + _result BOOLEAN; _is_close_friend BOOLEAN; BEGIN _account_id := vibetype.invoker_account_id(); - SELECT f.id - INTO _id + SELECT TRUE + INTO _result FROM vibetype.friendship f WHERE f.account_id = _account_id AND f.friend_account_id = friendship_toggle_closeness.friend_account_id; - IF _id IS NULL THEN + IF _result IS NULL THEN RAISE EXCEPTION 'Friendship does not exist' USING ERRCODE = 'VTFTC'; END IF; - UPDATE vibetype.friendship f + UPDATE vibetype.friendship_closeness f SET is_close_friend = NOT is_close_friend WHERE account_id = vibetype.invoker_account_id() AND f.friend_account_id = friendship_toggle_closeness.friend_account_id diff --git a/src/deploy/table_friendship.sql b/src/deploy/table_friendship.sql index 73648a13..2d824307 100644 --- a/src/deploy/table_friendship.sql +++ b/src/deploy/table_friendship.sql @@ -1,3 +1,5 @@ +BEGIN; + ----------------------------------------------------------- -- TABLE vibetype.friendship_request ----------------------------------------------------------- @@ -53,12 +55,8 @@ CREATE TABLE vibetype.friendship ( account_id UUID NOT NULL REFERENCES vibetype.account(id) ON DELETE CASCADE, friend_account_id UUID NOT NULL REFERENCES vibetype.account(id) ON DELETE CASCADE, - is_close_friend BOOLEAN NOT NULL DEFAULT FALSE, - created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID NOT NULL REFERENCES vibetype.account(id) ON DELETE CASCADE, - updated_at TIMESTAMP WITH TIME ZONE, - updated_by UUID REFERENCES vibetype.account(id) ON DELETE SET NULL, UNIQUE (account_id, friend_account_id), CONSTRAINT friendship_creator_friend CHECK (account_id <> friend_account_id), @@ -66,28 +64,16 @@ CREATE TABLE vibetype.friendship ( ); CREATE INDEX idx_friendship_created_by ON vibetype.friendship USING btree (created_by); -CREATE INDEX idx_friendship_updated_by ON vibetype.friendship USING btree (updated_by); COMMENT ON TABLE vibetype.friendship IS 'A friend relation together with its status.'; COMMENT ON COLUMN vibetype.friendship.id IS E'@omit create,update\nThe friend relation''s internal id.'; -COMMENT ON COLUMN vibetype.friendship.account_id IS E'@omit update\nThe one side of the friend relation. If the status is ''requested'' then it is the requestor account.'; -COMMENT ON COLUMN vibetype.friendship.friend_account_id IS E'@omit update\nThe other side of the friend relation. If the status is ''requested'' then it is the requestee account.'; -COMMENT ON COLUMN vibetype.friendship.is_close_friend IS E'@omit create\nThe flag indicating whether account_id considers friend_account_id as a close friend or not.'; +COMMENT ON COLUMN vibetype.friendship.account_id IS E'@omit update\nOne side of the friend relation.'; +COMMENT ON COLUMN vibetype.friendship.friend_account_id IS E'@omit update\nThe other side of the friend relation.'; COMMENT ON COLUMN vibetype.friendship.created_at IS E'@omit create,update\nThe timestamp when the friend relation was created.'; COMMENT ON COLUMN vibetype.friendship.created_by IS E'@omit update\nThe account that created the friend relation was created.'; -COMMENT ON COLUMN vibetype.friendship.updated_at IS E'@omit create,update\nThe timestamp when the friend relation''s status was updated.'; -COMMENT ON COLUMN vibetype.friendship.updated_by IS E'@omit create,update\nThe account that updated the friend relation''s status.'; COMMENT ON INDEX vibetype.idx_friendship_created_by IS 'B-Tree index to optimize lookups by creator.'; -COMMENT ON INDEX vibetype.idx_friendship_updated_by IS 'B-Tree index to optimize lookups by updater.'; - -CREATE TRIGGER vibetype_trigger_friendship_update - BEFORE - UPDATE - ON vibetype.friendship - FOR EACH ROW - EXECUTE PROCEDURE vibetype.trigger_metadata_update(); -GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE vibetype.friendship TO vibetype_account; +GRANT SELECT, INSERT, DELETE ON TABLE vibetype.friendship TO vibetype_account; ALTER TABLE vibetype.friendship ENABLE ROW LEVEL SECURITY; @@ -100,9 +86,7 @@ USING ( -- Only allow interactions with friendships in which the current user is involved. CREATE POLICY friendship_select ON vibetype.friendship FOR SELECT USING ( - account_id = vibetype.invoker_account_id() - OR - friend_account_id = vibetype.invoker_account_id() + TRUE ); -- Only allow creation by the current user and only if a friendship request is present. @@ -121,18 +105,93 @@ WITH CHECK ( ) ); --- Only allow update by the current user. -CREATE POLICY friendship_update ON vibetype.friendship FOR UPDATE +-- Only allow deletion if the current user is involved in the friendship. +CREATE POLICY friendship_delete ON vibetype.friendship FOR DELETE USING ( account_id = vibetype.invoker_account_id() -) WITH CHECK ( - updated_by = vibetype.invoker_account_id() + OR + friend_account_id = vibetype.invoker_account_id() ); --- Only allow deletion if the current user is involved in the friendship. -CREATE POLICY friendship_delete ON vibetype.friendship FOR DELETE +----------------------------------------------------------- +-- TABLE vibetype.friendship_closeness +----------------------------------------------------------- + +CREATE TABLE vibetype.friendship_closeness ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + account_id UUID NOT NULL, + friend_account_id UUID NOT NULL, + + is_close_friend BOOLEAN NOT NULL DEFAuLT FALSE, + + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_by UUID NOT NULL REFERENCES vibetype.account(id) ON DELETE CASCADE, + updated_at TIMESTAMP WITH TIME ZONE, + updated_by UUID REFERENCES vibetype.account(id) ON DELETE SET NULL, + + UNIQUE (account_id, friend_account_id), + CONSTRAINT fk_friendship_closeness FOREIGN KEY (account_id, friend_account_id) REFERENCES vibetype.friendship(account_id, friend_account_id) ON DELETE CASCADE, + CONSTRAINT friendship_closeness_creator CHECK (created_by = account_id), + CONSTRAINT friendship_closeness_updater CHECK (updated_by = account_id) +); + +CREATE INDEX idx_friendship_closeness_created_by ON vibetype.friendship_closeness USING btree (created_by); +CREATE INDEX idx_friendship_closeness_updated_by ON vibetype.friendship_closeness USING btree (updated_by); + +COMMENT ON TABLE vibetype.friendship_closeness IS 'The presence of a row in this tables indicates that account_id considers friend_account_id as a close friend.'; +COMMENT ON COLUMN vibetype.friendship_closeness.id IS E'@omit create,update\nThe friend relation''s internal id.'; +COMMENT ON COLUMN vibetype.friendship_closeness.account_id IS E'@omit update\nThe one side of the friend relation. If the status is ''requested'' then it is the requestor account.'; +COMMENT ON COLUMN vibetype.friendship_closeness.friend_account_id IS E'@omit update\nThe other side of the friend relation. If the status is ''requested'' then it is the requestee account.'; +COMMENT ON COLUMN vibetype.friendship_closeness.is_close_friend IS E'@omit create\nThe flag indicating whether account_id considers friend_account_id as a close friend or not.'; +COMMENT ON COLUMN vibetype.friendship_closeness.created_at IS E'@omit create,update\nThe timestamp when the friend relation was created.'; +COMMENT ON COLUMN vibetype.friendship_closeness.created_by IS E'@omit update\nThe account that created the friend relation was created.'; +COMMENT ON COLUMN vibetype.friendship_closeness.updated_at IS E'@omit create,update\nThe timestamp when the friend relation''s closeness status was updated.'; +COMMENT ON COLUMN vibetype.friendship_closeness.updated_by IS E'@omit create,update\nThe account that updated the friend relation''s closeness status.'; + +CREATE TRIGGER vibetype_trigger_friendship_closeness_update + BEFORE + UPDATE + ON vibetype.friendship_closeness + FOR EACH ROW + EXECUTE PROCEDURE vibetype.trigger_metadata_update(); + +GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE vibetype.friendship_closeness TO vibetype_account; + +ALTER TABLE vibetype.friendship_closeness ENABLE ROW LEVEL SECURITY; + +CREATE POLICY friendship_closeness_not_blocked ON vibetype.friendship_closeness AS RESTRICTIVE FOR ALL +USING ( + account_id NOT IN (SELECT id FROM vibetype_private.account_block_ids()) + AND friend_account_id NOT IN (SELECT id FROM vibetype_private.account_block_ids()) +); + +-- Only allow selection of close friends of the current account. +CREATE POLICY friendship_closeness_select ON vibetype.friendship_closeness FOR SELECT USING ( account_id = vibetype.invoker_account_id() +); + +-- Only allow creation by the current user and only if a friendship request is present. +CREATE POLICY friendship_closeness_insert ON vibetype.friendship_closeness FOR INSERT +WITH CHECK ( + account_id = vibetype.invoker_account_id() OR friend_account_id = vibetype.invoker_account_id() ); + +CREATE POLICY friendship_closeness_update ON vibetype.friendship_closeness FOR UPDATE +USING ( + account_id = vibetype.invoker_account_id() +) +WITH CHECK ( + updated_by = vibetype.invoker_account_id() +); + +-- Only allow deletion if the current user is involved in the friendship. +CREATE POLICY friendship_closeness_delete ON vibetype.friendship_closeness FOR DELETE +USING ( + account_id = vibetype.invoker_account_id() +); + +COMMIT; diff --git a/src/revert/table_friendship.sql b/src/revert/table_friendship.sql index d55c1efa..f717fdb6 100644 --- a/src/revert/table_friendship.sql +++ b/src/revert/table_friendship.sql @@ -1,17 +1,29 @@ BEGIN; +-- vibetype.friendship_closeness + +DROP POLICY friendship_closeness_not_blocked ON vibetype.friendship_closeness; +DROP POLICY friendship_closeness_select ON vibetype.friendship_closeness; +DROP POLICY friendship_closeness_insert ON vibetype.friendship_closeness; +DROP POLICY friendship_closeness_update ON vibetype.friendship_closeness; +DROP POLICY friendship_closeness_delete ON vibetype.friendship_closeness; + +DROP INDEX vibetype.idx_friendship_closeness_updated_by; +DROP INDEX vibetype.idx_friendship_closeness_created_by; + +DROP TRIGGER vibetype_trigger_friendship_closeness_update ON vibetype.friendship_closeness; + +DROP TABLE vibetype.friendship_closeness; + -- vibetype.friendship DROP POLICY friendship_not_blocked ON vibetype.friendship; DROP POLICY friendship_select ON vibetype.friendship; DROP POLICY friendship_insert ON vibetype.friendship; -DROP POLICY friendship_update ON vibetype.friendship; DROP POLICY friendship_delete ON vibetype.friendship; -DROP TRIGGER vibetype_trigger_friendship_update ON vibetype.friendship; - -DROP INDEX vibetype.idx_friendship_updated_by; DROP INDEX vibetype.idx_friendship_created_by; + DROP TABLE vibetype.friendship; -- vibetype.friendship_request diff --git a/src/verify/table_friendship.sql b/src/verify/table_friendship.sql index ff53670a..f6fd07d4 100644 --- a/src/verify/table_friendship.sql +++ b/src/verify/table_friendship.sql @@ -9,6 +9,15 @@ SELECT FROM vibetype.friendship_request WHERE FALSE; +SELECT + id, + account_id, + friend_account_id, + created_at, + created_by +FROM vibetype.friendship +WHERE FALSE; + SELECT id, account_id, @@ -18,7 +27,7 @@ SELECT created_by, updated_at, updated_by -FROM vibetype.friendship +FROM vibetype.friendship_closeness WHERE FALSE; ROLLBACK; @@ -32,7 +41,7 @@ DO $$ BEGIN ASSERT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.friendship', 'SELECT')); ASSERT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.friendship', 'INSERT')); - ASSERT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.friendship', 'UPDATE')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.friendship', 'UPDATE')); ASSERT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.friendship', 'DELETE')); ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_anonymous', 'vibetype.friendship', 'SELECT')); ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_anonymous', 'vibetype.friendship', 'INSERT')); @@ -56,6 +65,19 @@ BEGIN ASSERT NOT (SELECT pg_catalog.has_table_privilege(current_setting('role.vibetype_username'), 'vibetype.friendship_request', 'UPDATE')); ASSERT NOT (SELECT pg_catalog.has_table_privilege(current_setting('role.vibetype_username'), 'vibetype.friendship_request', 'DELETE')); + ASSERT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.friendship_closeness', 'SELECT')); + ASSERT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.friendship_closeness', 'INSERT')); + ASSERT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.friendship_closeness', 'UPDATE')); + ASSERT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.friendship_closeness', 'DELETE')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_anonymous', 'vibetype.friendship_closeness', 'SELECT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_anonymous', 'vibetype.friendship_closeness', 'INSERT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_anonymous', 'vibetype.friendship_closeness', 'UPDATE')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_anonymous', 'vibetype.friendship_closeness', 'DELETE')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege(current_setting('role.vibetype_username'), 'vibetype.friendship_closeness', 'SELECT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege(current_setting('role.vibetype_username'), 'vibetype.friendship_closeness', 'INSERT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege(current_setting('role.vibetype_username'), 'vibetype.friendship_closeness', 'UPDATE')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege(current_setting('role.vibetype_username'), 'vibetype.friendship_closeness', 'DELETE')); + END $$; ROLLBACK; diff --git a/test/fixture/schema_vibetype.definition.sql b/test/fixture/schema_vibetype.definition.sql index 7912fc0b..e50f877a 100644 --- a/test/fixture/schema_vibetype.definition.sql +++ b/test/fixture/schema_vibetype.definition.sql @@ -1318,6 +1318,12 @@ BEGIN INSERT INTO vibetype.friendship(account_id, friend_account_id, created_by) VALUES (_friend_account_id, requestor_account_id, _friend_account_id); + INSERT INTO vibetype.friendship_closeness(account_id, friend_account_id, created_by) + VALUES (requestor_account_id, _friend_account_id, requestor_account_id); + + INSERT INTO vibetype.friendship_closeness(account_id, friend_account_id, created_by) + VALUES (_friend_account_id, requestor_account_id, _friend_account_id); + DELETE FROM vibetype.friendship_request WHERE account_id = requestor_account_id AND friend_account_id = vibetype.invoker_account_id(); @@ -1484,23 +1490,23 @@ CREATE FUNCTION vibetype.friendship_toggle_closeness(friend_account_id uuid) RET AS $$ DECLARE _account_id UUID; - _id UUID; + _result BOOLEAN; _is_close_friend BOOLEAN; BEGIN _account_id := vibetype.invoker_account_id(); - SELECT f.id - INTO _id + SELECT TRUE + INTO _result FROM vibetype.friendship f WHERE f.account_id = _account_id AND f.friend_account_id = friendship_toggle_closeness.friend_account_id; - IF _id IS NULL THEN + IF _result IS NULL THEN RAISE EXCEPTION 'Friendship does not exist' USING ERRCODE = 'VTFTC'; END IF; - UPDATE vibetype.friendship f + UPDATE vibetype.friendship_closeness f SET is_close_friend = NOT is_close_friend WHERE account_id = vibetype.invoker_account_id() AND f.friend_account_id = friendship_toggle_closeness.friend_account_id @@ -3528,11 +3534,8 @@ CREATE TABLE vibetype.friendship ( id uuid DEFAULT gen_random_uuid() NOT NULL, account_id uuid NOT NULL, friend_account_id uuid NOT NULL, - is_close_friend boolean DEFAULT false NOT NULL, created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, created_by uuid NOT NULL, - updated_at timestamp with time zone, - updated_by uuid, CONSTRAINT friendship_creator_friend CHECK ((account_id <> friend_account_id)), CONSTRAINT friendship_creator_participant CHECK ((created_by = account_id)) ); @@ -3560,7 +3563,7 @@ The friend relation''s internal id.'; -- COMMENT ON COLUMN vibetype.friendship.account_id IS '@omit update -The one side of the friend relation. If the status is ''requested'' then it is the requestor account.'; +One side of the friend relation.'; -- @@ -3568,47 +3571,114 @@ The one side of the friend relation. If the status is ''requested'' then it is t -- COMMENT ON COLUMN vibetype.friendship.friend_account_id IS '@omit update +The other side of the friend relation.'; + + +-- +-- Name: COLUMN friendship.created_at; Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON COLUMN vibetype.friendship.created_at IS '@omit create,update +The timestamp when the friend relation was created.'; + + +-- +-- Name: COLUMN friendship.created_by; Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON COLUMN vibetype.friendship.created_by IS '@omit update +The account that created the friend relation was created.'; + + +-- +-- Name: friendship_closeness; Type: TABLE; Schema: vibetype; Owner: ci +-- + +CREATE TABLE vibetype.friendship_closeness ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + account_id uuid NOT NULL, + friend_account_id uuid NOT NULL, + is_close_friend boolean DEFAULT false NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + created_by uuid NOT NULL, + updated_at timestamp with time zone, + updated_by uuid, + CONSTRAINT friendship_closeness_creator CHECK ((created_by = account_id)), + CONSTRAINT friendship_closeness_updater CHECK ((updated_by = account_id)) +); + + +ALTER TABLE vibetype.friendship_closeness OWNER TO ci; + +-- +-- Name: TABLE friendship_closeness; Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON TABLE vibetype.friendship_closeness IS 'The presence of a row in this tables indicates that account_id considers friend_account_id as a close friend.'; + + +-- +-- Name: COLUMN friendship_closeness.id; Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON COLUMN vibetype.friendship_closeness.id IS '@omit create,update +The friend relation''s internal id.'; + + +-- +-- Name: COLUMN friendship_closeness.account_id; Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON COLUMN vibetype.friendship_closeness.account_id IS '@omit update +The one side of the friend relation. If the status is ''requested'' then it is the requestor account.'; + + +-- +-- Name: COLUMN friendship_closeness.friend_account_id; Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON COLUMN vibetype.friendship_closeness.friend_account_id IS '@omit update The other side of the friend relation. If the status is ''requested'' then it is the requestee account.'; -- --- Name: COLUMN friendship.is_close_friend; Type: COMMENT; Schema: vibetype; Owner: ci +-- Name: COLUMN friendship_closeness.is_close_friend; Type: COMMENT; Schema: vibetype; Owner: ci -- -COMMENT ON COLUMN vibetype.friendship.is_close_friend IS '@omit create +COMMENT ON COLUMN vibetype.friendship_closeness.is_close_friend IS '@omit create The flag indicating whether account_id considers friend_account_id as a close friend or not.'; -- --- Name: COLUMN friendship.created_at; Type: COMMENT; Schema: vibetype; Owner: ci +-- Name: COLUMN friendship_closeness.created_at; Type: COMMENT; Schema: vibetype; Owner: ci -- -COMMENT ON COLUMN vibetype.friendship.created_at IS '@omit create,update +COMMENT ON COLUMN vibetype.friendship_closeness.created_at IS '@omit create,update The timestamp when the friend relation was created.'; -- --- Name: COLUMN friendship.created_by; Type: COMMENT; Schema: vibetype; Owner: ci +-- Name: COLUMN friendship_closeness.created_by; Type: COMMENT; Schema: vibetype; Owner: ci -- -COMMENT ON COLUMN vibetype.friendship.created_by IS '@omit update +COMMENT ON COLUMN vibetype.friendship_closeness.created_by IS '@omit update The account that created the friend relation was created.'; -- --- Name: COLUMN friendship.updated_at; Type: COMMENT; Schema: vibetype; Owner: ci +-- Name: COLUMN friendship_closeness.updated_at; Type: COMMENT; Schema: vibetype; Owner: ci -- -COMMENT ON COLUMN vibetype.friendship.updated_at IS '@omit create,update -The timestamp when the friend relation''s status was updated.'; +COMMENT ON COLUMN vibetype.friendship_closeness.updated_at IS '@omit create,update +The timestamp when the friend relation''s closeness status was updated.'; -- --- Name: COLUMN friendship.updated_by; Type: COMMENT; Schema: vibetype; Owner: ci +-- Name: COLUMN friendship_closeness.updated_by; Type: COMMENT; Schema: vibetype; Owner: ci -- -COMMENT ON COLUMN vibetype.friendship.updated_by IS '@omit create,update -The account that updated the friend relation''s status.'; +COMMENT ON COLUMN vibetype.friendship_closeness.updated_by IS '@omit create,update +The account that updated the friend relation''s closeness status.'; -- @@ -4839,6 +4909,22 @@ ALTER TABLE ONLY vibetype.friendship ADD CONSTRAINT friendship_account_id_friend_account_id_key UNIQUE (account_id, friend_account_id); +-- +-- Name: friendship_closeness friendship_closeness_account_id_friend_account_id_key; Type: CONSTRAINT; Schema: vibetype; Owner: ci +-- + +ALTER TABLE ONLY vibetype.friendship_closeness + ADD CONSTRAINT friendship_closeness_account_id_friend_account_id_key UNIQUE (account_id, friend_account_id); + + +-- +-- Name: friendship_closeness friendship_closeness_pkey; Type: CONSTRAINT; Schema: vibetype; Owner: ci +-- + +ALTER TABLE ONLY vibetype.friendship_closeness + ADD CONSTRAINT friendship_closeness_pkey PRIMARY KEY (id); + + -- -- Name: friendship friendship_pkey; Type: CONSTRAINT; Schema: vibetype; Owner: ci -- @@ -5193,31 +5279,31 @@ COMMENT ON INDEX vibetype.idx_event_upload_is_header_image_unique IS 'Ensures th -- --- Name: idx_friendship_created_by; Type: INDEX; Schema: vibetype; Owner: ci +-- Name: idx_friendship_closeness_created_by; Type: INDEX; Schema: vibetype; Owner: ci -- -CREATE INDEX idx_friendship_created_by ON vibetype.friendship USING btree (created_by); +CREATE INDEX idx_friendship_closeness_created_by ON vibetype.friendship_closeness USING btree (created_by); -- --- Name: INDEX idx_friendship_created_by; Type: COMMENT; Schema: vibetype; Owner: ci +-- Name: idx_friendship_closeness_updated_by; Type: INDEX; Schema: vibetype; Owner: ci -- -COMMENT ON INDEX vibetype.idx_friendship_created_by IS 'B-Tree index to optimize lookups by creator.'; +CREATE INDEX idx_friendship_closeness_updated_by ON vibetype.friendship_closeness USING btree (updated_by); -- --- Name: idx_friendship_updated_by; Type: INDEX; Schema: vibetype; Owner: ci +-- Name: idx_friendship_created_by; Type: INDEX; Schema: vibetype; Owner: ci -- -CREATE INDEX idx_friendship_updated_by ON vibetype.friendship USING btree (updated_by); +CREATE INDEX idx_friendship_created_by ON vibetype.friendship USING btree (created_by); -- --- Name: INDEX idx_friendship_updated_by; Type: COMMENT; Schema: vibetype; Owner: ci +-- Name: INDEX idx_friendship_created_by; Type: COMMENT; Schema: vibetype; Owner: ci -- -COMMENT ON INDEX vibetype.idx_friendship_updated_by IS 'B-Tree index to optimize lookups by updater.'; +COMMENT ON INDEX vibetype.idx_friendship_created_by IS 'B-Tree index to optimize lookups by creator.'; -- @@ -5305,10 +5391,10 @@ CREATE TRIGGER vibetype_trigger_event_search_vector BEFORE INSERT OR UPDATE OF n -- --- Name: friendship vibetype_trigger_friendship_update; Type: TRIGGER; Schema: vibetype; Owner: ci +-- Name: friendship_closeness vibetype_trigger_friendship_closeness_update; Type: TRIGGER; Schema: vibetype; Owner: ci -- -CREATE TRIGGER vibetype_trigger_friendship_update BEFORE UPDATE ON vibetype.friendship FOR EACH ROW EXECUTE FUNCTION vibetype.trigger_metadata_update(); +CREATE TRIGGER vibetype_trigger_friendship_closeness_update BEFORE UPDATE ON vibetype.friendship_closeness FOR EACH ROW EXECUTE FUNCTION vibetype.trigger_metadata_update(); -- @@ -5524,6 +5610,14 @@ ALTER TABLE ONLY vibetype.event_upload ADD CONSTRAINT event_upload_upload_id_fkey FOREIGN KEY (upload_id) REFERENCES vibetype.upload(id) ON DELETE CASCADE; +-- +-- Name: friendship_closeness fk_friendship_closeness; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci +-- + +ALTER TABLE ONLY vibetype.friendship_closeness + ADD CONSTRAINT fk_friendship_closeness FOREIGN KEY (account_id, friend_account_id) REFERENCES vibetype.friendship(account_id, friend_account_id) ON DELETE CASCADE; + + -- -- Name: friendship friendship_account_id_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci -- @@ -5532,6 +5626,22 @@ ALTER TABLE ONLY vibetype.friendship ADD CONSTRAINT friendship_account_id_fkey FOREIGN KEY (account_id) REFERENCES vibetype.account(id) ON DELETE CASCADE; +-- +-- Name: friendship_closeness friendship_closeness_created_by_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci +-- + +ALTER TABLE ONLY vibetype.friendship_closeness + ADD CONSTRAINT friendship_closeness_created_by_fkey FOREIGN KEY (created_by) REFERENCES vibetype.account(id) ON DELETE CASCADE; + + +-- +-- Name: friendship_closeness friendship_closeness_updated_by_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci +-- + +ALTER TABLE ONLY vibetype.friendship_closeness + ADD CONSTRAINT friendship_closeness_updated_by_fkey FOREIGN KEY (updated_by) REFERENCES vibetype.account(id) ON DELETE SET NULL; + + -- -- Name: friendship friendship_created_by_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci -- @@ -5572,14 +5682,6 @@ ALTER TABLE ONLY vibetype.friendship_request ADD CONSTRAINT friendship_request_friend_account_id_fkey FOREIGN KEY (friend_account_id) REFERENCES vibetype.account(id) ON DELETE CASCADE; --- --- Name: friendship friendship_updated_by_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci --- - -ALTER TABLE ONLY vibetype.friendship - ADD CONSTRAINT friendship_updated_by_fkey FOREIGN KEY (updated_by) REFERENCES vibetype.account(id) ON DELETE SET NULL; - - -- -- Name: guest guest_contact_id_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci -- @@ -6017,6 +6119,49 @@ CREATE POLICY event_upload_select ON vibetype.event_upload FOR SELECT USING ((ev ALTER TABLE vibetype.friendship ENABLE ROW LEVEL SECURITY; +-- +-- Name: friendship_closeness; Type: ROW SECURITY; Schema: vibetype; Owner: ci +-- + +ALTER TABLE vibetype.friendship_closeness ENABLE ROW LEVEL SECURITY; + +-- +-- Name: friendship_closeness friendship_closeness_delete; Type: POLICY; Schema: vibetype; Owner: ci +-- + +CREATE POLICY friendship_closeness_delete ON vibetype.friendship_closeness FOR DELETE USING ((account_id = vibetype.invoker_account_id())); + + +-- +-- Name: friendship_closeness friendship_closeness_insert; Type: POLICY; Schema: vibetype; Owner: ci +-- + +CREATE POLICY friendship_closeness_insert ON vibetype.friendship_closeness FOR INSERT WITH CHECK (((account_id = vibetype.invoker_account_id()) OR (friend_account_id = vibetype.invoker_account_id()))); + + +-- +-- Name: friendship_closeness friendship_closeness_not_blocked; Type: POLICY; Schema: vibetype; Owner: ci +-- + +CREATE POLICY friendship_closeness_not_blocked ON vibetype.friendship_closeness AS RESTRICTIVE USING (((NOT (account_id IN ( SELECT account_block_ids.id + FROM vibetype_private.account_block_ids() account_block_ids(id)))) AND (NOT (friend_account_id IN ( SELECT account_block_ids.id + FROM vibetype_private.account_block_ids() account_block_ids(id)))))); + + +-- +-- Name: friendship_closeness friendship_closeness_select; Type: POLICY; Schema: vibetype; Owner: ci +-- + +CREATE POLICY friendship_closeness_select ON vibetype.friendship_closeness FOR SELECT USING ((account_id = vibetype.invoker_account_id())); + + +-- +-- Name: friendship_closeness friendship_closeness_update; Type: POLICY; Schema: vibetype; Owner: ci +-- + +CREATE POLICY friendship_closeness_update ON vibetype.friendship_closeness FOR UPDATE USING ((account_id = vibetype.invoker_account_id())) WITH CHECK ((updated_by = vibetype.invoker_account_id())); + + -- -- Name: friendship friendship_delete; Type: POLICY; Schema: vibetype; Owner: ci -- @@ -6088,14 +6233,7 @@ CREATE POLICY friendship_request_select ON vibetype.friendship_request FOR SELEC -- Name: friendship friendship_select; Type: POLICY; Schema: vibetype; Owner: ci -- -CREATE POLICY friendship_select ON vibetype.friendship FOR SELECT USING (((account_id = vibetype.invoker_account_id()) OR (friend_account_id = vibetype.invoker_account_id()))); - - --- --- Name: friendship friendship_update; Type: POLICY; Schema: vibetype; Owner: ci --- - -CREATE POLICY friendship_update ON vibetype.friendship FOR UPDATE USING ((account_id = vibetype.invoker_account_id())) WITH CHECK ((updated_by = vibetype.invoker_account_id())); +CREATE POLICY friendship_select ON vibetype.friendship FOR SELECT USING (true); -- @@ -6925,7 +7063,14 @@ GRANT SELECT,INSERT,DELETE ON TABLE vibetype.event_upload TO vibetype_account; -- Name: TABLE friendship; Type: ACL; Schema: vibetype; Owner: ci -- -GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE vibetype.friendship TO vibetype_account; +GRANT SELECT,INSERT,DELETE ON TABLE vibetype.friendship TO vibetype_account; + + +-- +-- Name: TABLE friendship_closeness; Type: ACL; Schema: vibetype; Owner: ci +-- + +GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE vibetype.friendship_closeness TO vibetype_account; -- diff --git a/test/logic/scenario/database/index.sql b/test/logic/scenario/database/index.sql index f397cb56..8c333049 100644 --- a/test/logic/scenario/database/index.sql +++ b/test/logic/scenario/database/index.sql @@ -153,7 +153,11 @@ SELECT vibetype_test.index_existence( ); SELECT vibetype_test.index_existence( - ARRAY ['idx_friendship_created_by', 'idx_friendship_updated_by'] + ARRAY ['idx_friendship_created_by'] +); + +SELECT vibetype_test.index_existence( + ARRAY ['idx_friendship_closeness_created_by', 'idx_friendship_closeness_updated_by'] ); SELECT vibetype_test.index_existence( diff --git a/test/logic/scenario/model/friendship.sql b/test/logic/scenario/model/friendship.sql index 91536e16..23a54b5a 100644 --- a/test/logic/scenario/model/friendship.sql +++ b/test/logic/scenario/model/friendship.sql @@ -23,29 +23,29 @@ BEGIN PERFORM vibetype_test.friendship_request_test('after A sends request to B (1)', accountA, accountA, accountB, true); PERFORM vibetype_test.friendship_request_test('after A sends request to B (2)', accountB, accountA, accountB, true); - PERFORM vibetype_test.friendship_test('after A sends request to B (3)', accountA, accountA, accountB, null, 0); - PERFORM vibetype_test.friendship_test('after A sends request to B (4)', accountB, accountB, accountA, null, 0); + PERFORM vibetype_test.friendship_test('after A sends request to B (3)', accountA, accountA, accountB, false); + PERFORM vibetype_test.friendship_test('after A sends request to B (4)', accountB, accountB, accountA, false); PERFORM vibetype_test.friendship_request_test('C cannot seen the friendship request from A to B', accountC, accountA, accountB, false); -- B accepts A's friendship request PERFORM vibetype_test.friendship_accept(accountB, accountA); PERFORM vibetype_test.friendship_request_test('B accepts friendship request from A (1)', accountA, accountA, accountB, false); - PERFORM vibetype_test.friendship_test('B accepts friendship request from A (2)', accountA, accountA, accountB, false, 1); - PERFORM vibetype_test.friendship_test('B accepts friendship request from A (3)', accountB, accountB, accountA, false, 1); - PERFORM vibetype_test.friendship_test('C cannot seen the friendship between A and B (1)', accountC, accountA, accountB, null, 0); - PERFORM vibetype_test.friendship_test('C cannot seen the friendship between A and B (2)', accountC, accountB, accountA, null, 0); + PERFORM vibetype_test.friendship_test('B accepts friendship request from A (2)', accountA, accountA, accountB, true); + PERFORM vibetype_test.friendship_test('B accepts friendship request from A (3)', accountB, accountB, accountA, true); + PERFORM vibetype_test.friendship_test('C can also see the friendship between A and B (1)', accountC, accountA, accountB, true); + PERFORM vibetype_test.friendship_test('C can also see the friendship between A and B (2)', accountC, accountB, accountA, true); -- friendship request from user C to A PERFORM vibetype_test.friendship_request(accountC, accountA, 'de'); PERFORM vibetype_test.friendship_request_test('after C sends request to A (1)', accountC, accountC, accountA, true); - PERFORM vibetype_test.friendship_test('after C sends request to A (2)', accountC, accountC, accountA, null, 0); - PERFORM vibetype_test.friendship_test('after A sends request to B (3)', accountA, accountA, accountC, null, 0); - PERFORM vibetype_test.friendship_test('User B is still a friend of user A (1)', accountA, accountA, accountB, null, 1); - PERFORM vibetype_test.friendship_test('User A is still a friend of user B (2)', accountB, accountB, accountA, null, 1); - PERFORM vibetype_test.friendship_test('User C has no friends', accountC, accountC, null, null, 0); - + PERFORM vibetype_test.friendship_test('after C sends request to A (2)', accountC, accountC, accountA, false); + PERFORM vibetype_test.friendship_test('after A sends request to B (3)', accountA, accountA, accountC, false); + PERFORM vibetype_test.friendship_test('User B is still a friend of user A (1)', accountA, accountA, accountB, true); + PERFORM vibetype_test.friendship_test('User A is still a friend of user B (2)', accountB, accountB, accountA, true); + PERFORM vibetype_test.friendship_test('User C is not a friend of A', accountC, accountC, accountA, false); + PERFORM vibetype_test.friendship_test('User C is not a friend of B', accountC, accountC, accountA, false); BEGIN -- C sends another request to A, should lead to exception VTREQ PERFORM vibetype_test.friendship_request(accountA, accountC, 'de'); @@ -92,17 +92,16 @@ BEGIN -- A rejects friendship request from C PERFORM vibetype_test.friendship_reject(accountA, accountC); - PERFORM vibetype_test.friendship_test('After A rejected C''s friendship request (1)', accountA, accountC, accountA, null, 0); - PERFORM vibetype_test.friendship_test('After A rejected C''s friendship request (2)', accountC, accountC, accountA, null, 0); + PERFORM vibetype_test.friendship_test('After A rejected C''s friendship request (1)', accountA, accountC, accountA, false); + PERFORM vibetype_test.friendship_test('After A rejected C''s friendship request (2)', accountC, accountC, accountA, false); -- a new friendship request from user C to A, this time accepted by A PERFORM vibetype_test.friendship_request(accountC, accountA, 'de'); PERFORM vibetype_test.friendship_accept(accountA, accountC); - PERFORM vibetype_test.friendship_test('Count the number of A''s friends', accountA, accountA, null, null, 2); - PERFORM vibetype_test.friendship_test('C is a friend of A (1)', accountA, accountA, accountC, null, 1); - PERFORM vibetype_test.friendship_test('C is a friend of A (2)', accountA, accountC, accountA, null, 1); - PERFORM vibetype_test.friendship_test('C is a friend of A (3)', accountC, accountA, accountC, null, 1); - PERFORM vibetype_test.friendship_test('C is a friend of A (4)', accountC, accountC, accountA, null, 1); + PERFORM vibetype_test.friendship_test('C is a friend of A (1)', accountA, accountA, accountC, true); + PERFORM vibetype_test.friendship_test('C is a friend of A (2)', accountA, accountC, accountA, true); + PERFORM vibetype_test.friendship_test('C is a friend of A (3)', accountC, accountA, accountC, true); + PERFORM vibetype_test.friendship_test('C is a friend of A (4)', accountC, accountC, accountA, true); -- friendship request from user B to C PERFORM vibetype_test.friendship_request(accountB, accountC, 'de'); @@ -122,34 +121,35 @@ BEGIN END LOOP; */ - PERFORM vibetype_test.friendship_test('B marks A as a close friend (1)', accountB, accountB, accountA, true, 1); - PERFORM vibetype_test.friendship_test('B marks A as a close friend (2)', accountB, accountA, accountB, false, 1); - PERFORM vibetype_test.friendship_test('B marks A as a close friend (3)', accountA, accountB, accountA, true, 1); - PERFORM vibetype_test.friendship_test('B marks A as a close friend (4)', accountA, accountA, accountB, false, 1); + -- Test: closeness is also visible for the account that declared or cancelled this property + -- towards one of the account's friends + + PERFORM vibetype_test.friendship_closeness_test('B marks A as a close friend (1)', accountB, accountB, accountA, true); + PERFORM vibetype_test.friendship_closeness_test('B marks A as a close friend (2)', accountB, accountA, accountB, null); + PERFORM vibetype_test.friendship_closeness_test('B marks A as a close friend (3)', accountA, accountB, accountA, null); + PERFORM vibetype_test.friendship_closeness_test('B marks A as a close friend (4)', accountA, accountA, accountB, false); + PERFORM vibetype_test.friendship_closeness_test('B marks A as a close friend (5)', accountC, accountB, accountA, null); + PERFORM vibetype_test.friendship_closeness_test('B marks A as a close friend (6)', accountC, accountA, accountB, null); -- A marks B as a close friend PERFORM vibetype_test.friendship_toggle_closeness(accountA, accountB); - PERFORM vibetype_test.friendship_test('A marks B as a close friend (1)', accountB, accountB, accountA, true, 1); - PERFORM vibetype_test.friendship_test('A marks B as a close friend (2)', accountB, accountA, accountB, true, 1); - PERFORM vibetype_test.friendship_test('A marks B as a close friend (3)', accountA, accountB, accountA, true, 1); - PERFORM vibetype_test.friendship_test('A marks B as a close friend (4)', accountA, accountA, accountB, true, 1); + PERFORM vibetype_test.friendship_closeness_test('A marks B as a close friend (1)', accountB, accountB, accountA, true); + PERFORM vibetype_test.friendship_closeness_test('A marks B as a close friend (2)', accountB, accountA, accountB, null); + PERFORM vibetype_test.friendship_closeness_test('A marks B as a close friend (3)', accountA, accountB, accountA, null); + PERFORM vibetype_test.friendship_closeness_test('A marks B as a close friend (4)', accountA, accountA, accountB, true); + PERFORM vibetype_test.friendship_closeness_test('B marks A as a close friend (5)', accountC, accountB, accountA, null); + PERFORM vibetype_test.friendship_closeness_test('B marks A as a close friend (6)', accountC, accountA, accountB, null); -- B unmarks A as a close friend PERFORM vibetype_test.friendship_toggle_closeness(accountB, accountA); - PERFORM vibetype_test.friendship_test('B unmarks A as a close friend (1)', accountB, accountB, accountA, false, 1); - PERFORM vibetype_test.friendship_test('B unmarks A as a close friend (2)', accountB, accountA, accountB, true, 1); - PERFORM vibetype_test.friendship_test('B unmarks A as a close friend (3)', accountA, accountB, accountA, false, 1); - PERFORM vibetype_test.friendship_test('B unmarks A as a close friend (4)', accountA, accountA, accountB, true, 1); - - -- A cancels friendship with C - PERFORM vibetype_test.friendship_cancel(accountA, accountC); - - PERFORM vibetype_test.friendship_test('A cancels friendship with C (1)', accountA, accountC, accountA, null, 0); - PERFORM vibetype_test.friendship_test('A cancels friendship with C (2)', accountA, accountA, accountC, null, 0); - PERFORM vibetype_test.friendship_test('A cancels friendship with C (3)', accountC, accountC, accountA, null, 0); - PERFORM vibetype_test.friendship_test('A cancels friendship with C (4)', accountC, accountA, accountC, null, 0); + PERFORM vibetype_test.friendship_closeness_test('B unmarks A as a close friend (1)', accountB, accountB, accountA, false); + PERFORM vibetype_test.friendship_closeness_test('B unmarks A as a close friend (2)', accountB, accountA, accountB, null); + PERFORM vibetype_test.friendship_closeness_test('B unmarks A as a close friend (3)', accountA, accountB, accountA, null); + PERFORM vibetype_test.friendship_closeness_test('B unmarks A as a close friend (4)', accountA, accountA, accountB, true); + PERFORM vibetype_test.friendship_closeness_test('B marks A as a close friend (5)', accountC, accountB, accountA, null); + PERFORM vibetype_test.friendship_closeness_test('B marks A as a close friend (6)', accountC, accountA, accountB, null); END $$; diff --git a/test/logic/utility/model/friendship.sql b/test/logic/utility/model/friendship.sql index d8d7f233..6bf9ac20 100644 --- a/test/logic/utility/model/friendship.sql +++ b/test/logic/utility/model/friendship.sql @@ -86,29 +86,58 @@ CREATE OR REPLACE FUNCTION vibetype_test.friendship_test ( _invoker_account_id UUID, _account_id UUID, _friend_account_id UUID, -- _friend_account_id IS NULL means "any friend" - _is_close_friend BOOLEAN, -- _is_close_friend IS NULL means "any boolean value" - _expected_count INTEGER + _expected_result BOOLEAN ) RETURNS VOID AS $$ DECLARE - _result INTEGER; + _result BOOLEAN; BEGIN SET LOCAL role = 'vibetype_account'; EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || _invoker_account_id || ''''; - SELECT count(*) INTO _result + SELECT TRUE INTO _result FROM vibetype.friendship WHERE account_id = _account_id - AND (_friend_account_id IS NULL OR friend_account_id = _friend_account_id) - AND (_is_close_friend IS NULL OR is_close_friend = _is_close_friend); + AND friend_account_id = _friend_account_id; + + IF _result IS NULL THEN + _result := FALSE; + END IF; + + IF _result != _expected_result THEN + RAISE EXCEPTION '%: expected result was % but result is %.', _test_case, _expected_result, _result USING ERRCODE = 'VTTST'; + END IF; + + SET LOCAL ROLE NONE; +END $$ LANGUAGE plpgsql; + +GRANT EXECUTE ON FUNCTION vibetype_test.friendship_test(TEXT, UUID, UUID, UUID, BOOLEAN) TO vibetype_account; + +CREATE OR REPLACE FUNCTION vibetype_test.friendship_closeness_test ( + _test_case TEXT, + _invoker_account_id UUID, + _account_id UUID, + _friend_account_id UUID, + _expected_result BOOLEAN +) RETURNS VOID AS $$ +DECLARE + _result BOOLEAN; +BEGIN + SET LOCAL role = 'vibetype_account'; + EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || _invoker_account_id || ''''; + + SELECT is_close_friend INTO _result + FROM vibetype.friendship_closeness + WHERE account_id = _account_id + AND friend_account_id = _friend_account_id; - IF _result != _expected_count THEN - RAISE EXCEPTION '%: expected count was % but result is %.', _test_case, _expected_count, _result USING ERRCODE = 'VTTST'; + IF _result != _expected_result THEN + RAISE EXCEPTION '%: expected result was % but result is %.', _test_case, _expected_result, _result USING ERRCODE = 'VTFCT'; END IF; SET LOCAL ROLE NONE; END $$ LANGUAGE plpgsql; -GRANT EXECUTE ON FUNCTION vibetype_test.friendship_test(TEXT, UUID, UUID, UUID, BOOLEAN, INTEGER) TO vibetype_account; +GRANT EXECUTE ON FUNCTION vibetype_test.friendship_closeness_test(TEXT, UUID, UUID, UUID, BOOLEAN) TO vibetype_account; CREATE OR REPLACE FUNCTION vibetype_test.friendship_request_test ( From ce52a373d4c0bc8d337763234e6f022107f383ef Mon Sep 17 00:00:00 2001 From: Sven Thelemann Date: Wed, 20 Aug 2025 23:18:08 +0200 Subject: [PATCH 8/8] feat(friendship): read language from contact Function `friendship.request`reads the language from the account's own contact. The default language is null (in case the value is null in the contact). --- src/deploy/function_friendship.sql | 57 +++++++-------- src/revert/function_friendship.sql | 3 +- src/verify/function_friendship.sql | 8 +- test/fixture/schema_vibetype.definition.sql | 81 ++++++++------------- test/logic/scenario/model/friendship.sql | 16 ++-- test/logic/utility/model/friendship.sql | 8 +- 6 files changed, 70 insertions(+), 103 deletions(-) diff --git a/src/deploy/function_friendship.sql b/src/deploy/function_friendship.sql index 6f417531..8b326272 100644 --- a/src/deploy/function_friendship.sql +++ b/src/deploy/function_friendship.sql @@ -62,32 +62,6 @@ COMMENT ON FUNCTION vibetype.friendship_cancel(UUID) IS 'Cancels a friendship (i GRANT EXECUTE ON FUNCTION vibetype.friendship_cancel(UUID) TO vibetype_account; --- create notification for a request - -CREATE FUNCTION vibetype.friendship_notify_request( - friend_account_id UUID, - language TEXT -) RETURNS VOID AS $$ -BEGIN - - INSERT INTO vibetype_private.notification (channel, payload) - VALUES ( - 'friendship_request', - jsonb_pretty(jsonb_build_object( - 'data', jsonb_build_object( - 'requestor_account_id', vibetype.invoker_account_id(), - 'requestee_account_id', friendship_notify_request.friend_account_id - ), - 'template', jsonb_build_object('language', friendship_notify_request.language) - )) - ); - -END; $$ LANGUAGE plpgsql SECURITY DEFINER; - -COMMENT ON FUNCTION vibetype.friendship_notify_request(UUID, TEXT) IS 'Creates a notification for a friendship_request'; - -GRANT EXECUTE ON FUNCTION vibetype.friendship_notify_request(UUID, TEXT) TO vibetype_account; - -- reject friendship request CREATE FUNCTION vibetype.friendship_reject( @@ -107,15 +81,20 @@ GRANT EXECUTE ON FUNCTION vibetype.friendship_reject(UUID) TO vibetype_account; -- request friendship CREATE FUNCTION vibetype.friendship_request( - friend_account_id UUID, - language TEXT + friend_account_id UUID ) RETURNS VOID AS $$ DECLARE _account_id UUID; + _language TEXT; BEGIN _account_id := vibetype.invoker_account_id(); + IF _account_id IN (SELECT id FROM vibetype_private.account_block_ids()) + OR friend_account_id IN (SELECT id FROM vibetype_private.account_block_ids()) THEN + RETURN; + END IF; + IF EXISTS( SELECT 1 FROM vibetype.friendship f @@ -138,13 +117,27 @@ BEGIN INSERT INTO vibetype.friendship_request(account_id, friend_account_id, created_by) VALUES (_account_id, friendship_request.friend_account_id, _account_id); - PERFORM vibetype.friendship_notify_request(friendship_request.friend_account_id, friendship_request.language); + SELECT COALESCE(language::TEXT, 'de') INTO _language + FROM vibetype.contact + WHERE account_id = _account_id AND created_by = _account_id; -END; $$ LANGUAGE plpgsql SECURITY INVOKER; + INSERT INTO vibetype_private.notification (channel, payload) + VALUES ( + 'friendship_request', + jsonb_pretty(jsonb_build_object( + 'data', jsonb_build_object( + 'requestor_account_id', vibetype.invoker_account_id(), + 'requestee_account_id', friendship_request.friend_account_id + ), + 'template', jsonb_build_object('language', _language) + )) + ); + +END; $$ LANGUAGE plpgsql SECURITY DEFINER; -COMMENT ON FUNCTION vibetype.friendship_request(UUID, TEXT) IS E'Starts a new friendship request.\n\nError codes:\n- **VTFEX** when the friendship already exists.\n- **VTREQ** when there is already a friendship request.'; +COMMENT ON FUNCTION vibetype.friendship_request(UUID) IS E'Starts a new friendship request.\n\nError codes:\n- **VTFEX** when the friendship already exists.\n- **VTREQ** when there is already a friendship request.'; -GRANT EXECUTE ON FUNCTION vibetype.friendship_request(UUID, TEXT) TO vibetype_account; +GRANT EXECUTE ON FUNCTION vibetype.friendship_request(UUID) TO vibetype_account; -- toggle closeness of friendship diff --git a/src/revert/function_friendship.sql b/src/revert/function_friendship.sql index b02f5fdc..a1efcfe9 100644 --- a/src/revert/function_friendship.sql +++ b/src/revert/function_friendship.sql @@ -2,9 +2,8 @@ BEGIN; DROP FUNCTION vibetype.friendship_accept(UUID); DROP FUNCTION vibetype.friendship_cancel(UUID); -DROP FUNCTION vibetype.friendship_notify_request(UUID, TEXT); DROP FUNCTION vibetype.friendship_reject(UUID); -DROP FUNCTION vibetype.friendship_request(UUID, TEXT); +DROP FUNCTION vibetype.friendship_request(UUID); DROP FUNCTION vibetype.friendship_toggle_closeness(UUID); COMMIT; diff --git a/src/verify/function_friendship.sql b/src/verify/function_friendship.sql index 24c7f462..24079f4a 100644 --- a/src/verify/function_friendship.sql +++ b/src/verify/function_friendship.sql @@ -11,16 +11,12 @@ BEGIN RAISE EXCEPTION 'Test failed: vibetype_account does not have EXECUTE privilege for vibetype.friendship_cancel(UUID).'; END IF; - IF NOT (SELECT pg_catalog.has_function_privilege('vibetype_account', 'vibetype.friendship_notify_request(UUID, TEXT)', 'EXECUTE')) THEN - RAISE EXCEPTION 'Test failed: vibetype_account does not have EXECUTE privilege for vibetype.friendship_notify_request(UUID, TEXT).'; - END IF; - IF NOT (SELECT pg_catalog.has_function_privilege('vibetype_account', 'vibetype.friendship_reject(UUID)', 'EXECUTE')) THEN RAISE EXCEPTION 'Test failed: vibetype_account does not have EXECUTE privilege for vibetype.friendship_reject(UUID).'; END IF; - IF NOT (SELECT pg_catalog.has_function_privilege('vibetype_account', 'vibetype.friendship_request(UUID, TEXT)', 'EXECUTE')) THEN - RAISE EXCEPTION 'Test failed: vibetype_account does not have EXECUTE privilege for vibetype.friendship_request(UUID, TEXT).'; + IF NOT (SELECT pg_catalog.has_function_privilege('vibetype_account', 'vibetype.friendship_request(UUID)', 'EXECUTE')) THEN + RAISE EXCEPTION 'Test failed: vibetype_account does not have EXECUTE privilege for vibetype.friendship_request(UUID).'; END IF; IF NOT (SELECT pg_catalog.has_function_privilege('vibetype_account', 'vibetype.friendship_toggle_closeness(UUID)', 'EXECUTE')) THEN diff --git a/test/fixture/schema_vibetype.definition.sql b/test/fixture/schema_vibetype.definition.sql index e50f877a..916195da 100644 --- a/test/fixture/schema_vibetype.definition.sql +++ b/test/fixture/schema_vibetype.definition.sql @@ -1371,39 +1371,6 @@ ALTER FUNCTION vibetype.friendship_cancel(friend_account_id uuid) OWNER TO ci; COMMENT ON FUNCTION vibetype.friendship_cancel(friend_account_id uuid) IS 'Cancels a friendship (in both directions) if it exists.'; --- --- Name: friendship_notify_request(uuid, text); Type: FUNCTION; Schema: vibetype; Owner: ci --- - -CREATE FUNCTION vibetype.friendship_notify_request(friend_account_id uuid, language text) RETURNS void - LANGUAGE plpgsql SECURITY DEFINER - AS $$ -BEGIN - - INSERT INTO vibetype_private.notification (channel, payload) - VALUES ( - 'friendship_request', - jsonb_pretty(jsonb_build_object( - 'data', jsonb_build_object( - 'requestor_account_id', vibetype.invoker_account_id(), - 'requestee_account_id', friendship_notify_request.friend_account_id - ), - 'template', jsonb_build_object('language', friendship_notify_request.language) - )) - ); - -END; $$; - - -ALTER FUNCTION vibetype.friendship_notify_request(friend_account_id uuid, language text) OWNER TO ci; - --- --- Name: FUNCTION friendship_notify_request(friend_account_id uuid, language text); Type: COMMENT; Schema: vibetype; Owner: ci --- - -COMMENT ON FUNCTION vibetype.friendship_notify_request(friend_account_id uuid, language text) IS 'Creates a notification for a friendship_request'; - - -- -- Name: friendship_reject(uuid); Type: FUNCTION; Schema: vibetype; Owner: ci -- @@ -1429,18 +1396,24 @@ COMMENT ON FUNCTION vibetype.friendship_reject(requestor_account_id uuid) IS 'Re -- --- Name: friendship_request(uuid, text); Type: FUNCTION; Schema: vibetype; Owner: ci +-- Name: friendship_request(uuid); Type: FUNCTION; Schema: vibetype; Owner: ci -- -CREATE FUNCTION vibetype.friendship_request(friend_account_id uuid, language text) RETURNS void - LANGUAGE plpgsql +CREATE FUNCTION vibetype.friendship_request(friend_account_id uuid) RETURNS void + LANGUAGE plpgsql SECURITY DEFINER AS $$ DECLARE _account_id UUID; + _language TEXT; BEGIN _account_id := vibetype.invoker_account_id(); + IF _account_id IN (SELECT id FROM vibetype_private.account_block_ids()) + OR friend_account_id IN (SELECT id FROM vibetype_private.account_block_ids()) THEN + RETURN; + END IF; + IF EXISTS( SELECT 1 FROM vibetype.friendship f @@ -1463,18 +1436,32 @@ BEGIN INSERT INTO vibetype.friendship_request(account_id, friend_account_id, created_by) VALUES (_account_id, friendship_request.friend_account_id, _account_id); - PERFORM vibetype.friendship_notify_request(friendship_request.friend_account_id, friendship_request.language); + SELECT COALESCE(language::TEXT, 'de') INTO _language + FROM vibetype.contact + WHERE account_id = _account_id AND created_by = _account_id; + + INSERT INTO vibetype_private.notification (channel, payload) + VALUES ( + 'friendship_request', + jsonb_pretty(jsonb_build_object( + 'data', jsonb_build_object( + 'requestor_account_id', vibetype.invoker_account_id(), + 'requestee_account_id', friendship_request.friend_account_id + ), + 'template', jsonb_build_object('language', _language) + )) + ); END; $$; -ALTER FUNCTION vibetype.friendship_request(friend_account_id uuid, language text) OWNER TO ci; +ALTER FUNCTION vibetype.friendship_request(friend_account_id uuid) OWNER TO ci; -- --- Name: FUNCTION friendship_request(friend_account_id uuid, language text); Type: COMMENT; Schema: vibetype; Owner: ci +-- Name: FUNCTION friendship_request(friend_account_id uuid); Type: COMMENT; Schema: vibetype; Owner: ci -- -COMMENT ON FUNCTION vibetype.friendship_request(friend_account_id uuid, language text) IS 'Starts a new friendship request. +COMMENT ON FUNCTION vibetype.friendship_request(friend_account_id uuid) IS 'Starts a new friendship request. Error codes: - **VTFEX** when the friendship already exists. @@ -6677,14 +6664,6 @@ REVOKE ALL ON FUNCTION vibetype.friendship_cancel(friend_account_id uuid) FROM P GRANT ALL ON FUNCTION vibetype.friendship_cancel(friend_account_id uuid) TO vibetype_account; --- --- Name: FUNCTION friendship_notify_request(friend_account_id uuid, language text); Type: ACL; Schema: vibetype; Owner: ci --- - -REVOKE ALL ON FUNCTION vibetype.friendship_notify_request(friend_account_id uuid, language text) FROM PUBLIC; -GRANT ALL ON FUNCTION vibetype.friendship_notify_request(friend_account_id uuid, language text) TO vibetype_account; - - -- -- Name: FUNCTION friendship_reject(requestor_account_id uuid); Type: ACL; Schema: vibetype; Owner: ci -- @@ -6694,11 +6673,11 @@ GRANT ALL ON FUNCTION vibetype.friendship_reject(requestor_account_id uuid) TO v -- --- Name: FUNCTION friendship_request(friend_account_id uuid, language text); Type: ACL; Schema: vibetype; Owner: ci +-- Name: FUNCTION friendship_request(friend_account_id uuid); Type: ACL; Schema: vibetype; Owner: ci -- -REVOKE ALL ON FUNCTION vibetype.friendship_request(friend_account_id uuid, language text) FROM PUBLIC; -GRANT ALL ON FUNCTION vibetype.friendship_request(friend_account_id uuid, language text) TO vibetype_account; +REVOKE ALL ON FUNCTION vibetype.friendship_request(friend_account_id uuid) FROM PUBLIC; +GRANT ALL ON FUNCTION vibetype.friendship_request(friend_account_id uuid) TO vibetype_account; -- diff --git a/test/logic/scenario/model/friendship.sql b/test/logic/scenario/model/friendship.sql index 23a54b5a..b3579501 100644 --- a/test/logic/scenario/model/friendship.sql +++ b/test/logic/scenario/model/friendship.sql @@ -19,7 +19,7 @@ BEGIN PERFORM vibetype_test.friendship_request_test('before A sends request to B', accountA, accountA, accountB, false); -- friendship request from user A to B - PERFORM vibetype_test.friendship_request(accountA, accountB, 'de'); + PERFORM vibetype_test.friendship_request(accountA, accountB); PERFORM vibetype_test.friendship_request_test('after A sends request to B (1)', accountA, accountA, accountB, true); PERFORM vibetype_test.friendship_request_test('after A sends request to B (2)', accountB, accountA, accountB, true); @@ -37,7 +37,7 @@ BEGIN PERFORM vibetype_test.friendship_test('C can also see the friendship between A and B (2)', accountC, accountB, accountA, true); -- friendship request from user C to A - PERFORM vibetype_test.friendship_request(accountC, accountA, 'de'); + PERFORM vibetype_test.friendship_request(accountC, accountA); PERFORM vibetype_test.friendship_request_test('after C sends request to A (1)', accountC, accountC, accountA, true); PERFORM vibetype_test.friendship_test('after C sends request to A (2)', accountC, accountC, accountA, false); @@ -48,7 +48,7 @@ BEGIN PERFORM vibetype_test.friendship_test('User C is not a friend of B', accountC, accountC, accountA, false); BEGIN -- C sends another request to A, should lead to exception VTREQ - PERFORM vibetype_test.friendship_request(accountA, accountC, 'de'); + PERFORM vibetype_test.friendship_request(accountA, accountC); RAISE 'C sends another request to A: it was possible to request a friendship more than once.'; EXCEPTION WHEN OTHERS THEN @@ -59,7 +59,7 @@ BEGIN BEGIN -- A sends a request to C, should lead to exception VTREQ - PERFORM vibetype_test.friendship_request(accountA, accountC, 'de'); + PERFORM vibetype_test.friendship_request(accountA, accountC); RAISE 'A sends a request to C: it was possible to request a friendship more than once.'; EXCEPTION WHEN OTHERS THEN @@ -70,7 +70,7 @@ BEGIN BEGIN -- A sends a new request to B, should lead to exception VTFEX - PERFORM vibetype_test.friendship_request(accountA, accountB, 'de'); + PERFORM vibetype_test.friendship_request(accountA, accountB); RAISE 'A sends a new request to B: it was possible to request for an already existing friendship.'; EXCEPTION WHEN OTHERS THEN @@ -81,7 +81,7 @@ BEGIN BEGIN -- B sends a new request to A, should lead to exception VTFEX - PERFORM vibetype_test.friendship_request(accountB, accountA, 'de'); + PERFORM vibetype_test.friendship_request(accountB, accountA); RAISE 'It was possible to request for an already existing friendship.'; EXCEPTION WHEN OTHERS THEN @@ -96,7 +96,7 @@ BEGIN PERFORM vibetype_test.friendship_test('After A rejected C''s friendship request (2)', accountC, accountC, accountA, false); -- a new friendship request from user C to A, this time accepted by A - PERFORM vibetype_test.friendship_request(accountC, accountA, 'de'); + PERFORM vibetype_test.friendship_request(accountC, accountA); PERFORM vibetype_test.friendship_accept(accountA, accountC); PERFORM vibetype_test.friendship_test('C is a friend of A (1)', accountA, accountA, accountC, true); PERFORM vibetype_test.friendship_test('C is a friend of A (2)', accountA, accountC, accountA, true); @@ -104,7 +104,7 @@ BEGIN PERFORM vibetype_test.friendship_test('C is a friend of A (4)', accountC, accountC, accountA, true); -- friendship request from user B to C - PERFORM vibetype_test.friendship_request(accountB, accountC, 'de'); + PERFORM vibetype_test.friendship_request(accountB, accountC); -- B marks A as a close friend PERFORM vibetype_test.friendship_toggle_closeness(accountB, accountA); diff --git a/test/logic/utility/model/friendship.sql b/test/logic/utility/model/friendship.sql index 6bf9ac20..69e9626b 100644 --- a/test/logic/utility/model/friendship.sql +++ b/test/logic/utility/model/friendship.sql @@ -51,19 +51,19 @@ GRANT EXECUTE ON FUNCTION vibetype_test.friendship_reject(UUID, UUID) TO vibetyp CREATE OR REPLACE FUNCTION vibetype_test.friendship_request ( _invoker_account_id UUID, - _friend_account_id UUID, - _language TEXT + _friend_account_id UUID ) RETURNS VOID AS $$ BEGIN SET LOCAL role = 'vibetype_account'; EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || _invoker_account_id || ''''; - PERFORM vibetype.friendship_request(_friend_account_id, _language); + PERFORM vibetype.friendship_request(_friend_account_id); SET LOCAL ROLE NONE; END $$ LANGUAGE plpgsql; -GRANT EXECUTE ON FUNCTION vibetype_test.friendship_request(UUID, UUID, TEXT) TO vibetype_account; +GRANT EXECUTE ON FUNCTION vibetype_test.friendship_request(UUID, UUID) TO vibetype_account; + CREATE OR REPLACE FUNCTION vibetype_test.friendship_toggle_closeness ( _invoker_account_id UUID,