From a0722a1c5dcfcde83ef0176747eb2f2a9d78e1e9 Mon Sep 17 00:00:00 2001 From: Brendan Quinn Date: Tue, 1 Aug 2023 22:04:30 +0000 Subject: [PATCH] Allow for transferring all file sets from one to another, maintaining their relative ordering via the rank property. The remaining 'empty' work is subsequently deleted. --- .../Work/Tabs/Preservation/Preservation.jsx | 25 +++ .../Preservation/TransferFileSetsModal.jsx | 198 ++++++++++++++++++ .../TransferFileSetsModal.test.jsx | 40 ++++ app/assets/js/components/Work/work.gql.js | 8 + .../js/components/Work/work.gql.mock.js | 19 ++ app/lib/meadow/data.ex | 18 ++ .../meadow/data/works/transfer_file_sets.ex | 143 +++++++++++++ app/lib/meadow_web/resolvers/data.ex | 8 + .../schema/types/data/work_types.ex | 9 + app/test/gql/TransferFileSets.gql | 5 + .../data/works/transfer_file_sets_test.exs | 94 +++++++++ app/test/meadow/data/works_test.exs | 2 + .../mutation/transfer_file_sets_test.exs | 75 +++++++ app/test/support/test_helpers.ex | 2 +- 14 files changed, 645 insertions(+), 1 deletion(-) create mode 100644 app/assets/js/components/Work/Tabs/Preservation/TransferFileSetsModal.jsx create mode 100644 app/assets/js/components/Work/Tabs/Preservation/TransferFileSetsModal.test.jsx create mode 100644 app/lib/meadow/data/works/transfer_file_sets.ex create mode 100644 app/test/gql/TransferFileSets.gql create mode 100644 app/test/meadow/data/works/transfer_file_sets_test.exs create mode 100644 app/test/meadow_web/schema/mutation/transfer_file_sets_test.exs diff --git a/app/assets/js/components/Work/Tabs/Preservation/Preservation.jsx b/app/assets/js/components/Work/Tabs/Preservation/Preservation.jsx index 1501c5940..5c9d005c4 100644 --- a/app/assets/js/components/Work/Tabs/Preservation/Preservation.jsx +++ b/app/assets/js/components/Work/Tabs/Preservation/Preservation.jsx @@ -26,6 +26,7 @@ import PropTypes from "prop-types"; import UISkeleton from "@js/components/UI/Skeleton"; import UITabsStickyHeader from "@js/components/UI/Tabs/StickyHeader"; import WorkTabsPreservationFileSetModal from "@js/components/Work/Tabs/Preservation/FileSetModal"; +import WorkTabsPreservationTransferFileSetsModal from "@js/components/Work/Tabs/Preservation/TransferFileSetsModal"; import { useMutation, useQuery } from "@apollo/client"; import { sortFileSets, toastWrapper } from "@js/services/helpers"; import { formatDate } from "@js/services/helpers"; @@ -51,6 +52,10 @@ const WorkTabsPreservation = ({ work }) => { fileset: {}, isVisible: false, }); + const [transferFilesetsModal, setTransferFilesetsModal] = React.useState({ + fromWorkId: work.id, + isVisible: false, + }); const { data: verifyFileSetsData, @@ -58,6 +63,8 @@ const WorkTabsPreservation = ({ work }) => { loading: verifyFileSetsLoading, } = useQuery(VERIFY_FILE_SETS, { variables: { workId: work.id } }); + + /** * Delete a Fileset */ @@ -180,6 +187,10 @@ const WorkTabsPreservation = ({ work }) => { setTechnicalMetadata({ fileSet: { ...fileSet } }); }; + const handleTransferFileSetsClick = () => { + setTransferFilesetsModal({ fromWorkId: work.id, isVisible: true }); + }; + return (
@@ -304,9 +315,23 @@ const WorkTabsPreservation = ({ work }) => { )} +
+ setTransferFilesetsModal({ isVisible: false })} + isVisible={transferFilesetsModal.isVisible} + fromWorkId={work.id} + /> + setIsAddFilesetModalVisible(false)} isVisible={isAddFilesetModalVisible} diff --git a/app/assets/js/components/Work/Tabs/Preservation/TransferFileSetsModal.jsx b/app/assets/js/components/Work/Tabs/Preservation/TransferFileSetsModal.jsx new file mode 100644 index 000000000..38de036f7 --- /dev/null +++ b/app/assets/js/components/Work/Tabs/Preservation/TransferFileSetsModal.jsx @@ -0,0 +1,198 @@ +import React, { useState } from "react"; +import PropTypes from "prop-types"; +import { useHistory } from "react-router-dom"; +import { Button, Notification } from "@nulib/design-system"; +import { TRANSFER_FILE_SETS } from "@js/components/Work/work.gql.js"; +import { useMutation } from "@apollo/client"; +import { toastWrapper } from "@js/services/helpers"; +import { useForm, FormProvider } from "react-hook-form"; +import Error from "@js/components/UI/Error"; +import classNames from "classnames"; +import UIFormField from "@js/components/UI/Form/Field.jsx"; +import UIInput from "@js/components/UI/Form/Input"; +import { css } from "@emotion/react"; + +const modalCss = css` + z-index: 100; +`; + +function WorkTabsPreservationTransferFileSetsModal({ + closeModal, + isVisible, + fromWorkId, +}) { + const defaultValues = { + fromWorkId: null, + }; + + const [formError, setFormError] = useState(); + const [confirmationInput, setConfirmationInput] = useState(""); + const [confirmationError, setConfirmationError] = useState(); + const [isSubmitted, setIsSubmitted] = useState(false); + const [toWorkId, setToWorkId] = useState(""); + + const methods = useForm({ + defaultValues: defaultValues, + shouldUnregister: false, + }); + + const history = useHistory(); + + const handleToWorkIdChange = (e) => { + setToWorkId(e.target.value); + }; + + const [transferFileSets, { loading, error, data }] = useMutation( + TRANSFER_FILE_SETS, + { + onCompleted({ transferFileSets }) { + toastWrapper( + "is-success", + `FileSets transferred successfully to work: ${transferFileSets.id}` + ); + resetForm(); + history.push(`/work/${transferFileSets.id}`); + }, + onError(error) { + console.error( + "error in the transferFileSets GraphQL mutation :>> ", + error + ); + console.error("error MESSAGE", error.message) + console.error("graphQL ERRORS", error.graphQLErrors) + setFormError(error); + }, + } + ); + + const handleSubmit = (data) => { + setToWorkId(data.toWorkId); + setIsSubmitted(true); + if (confirmationInput !== "I understand") { + setConfirmationError({ + confirmationText: "Confirmation text is required.", + }); + return; + } + setConfirmationError(null); + + transferFileSets({ + variables: { + fromWorkId: fromWorkId, + toWorkId: data.toWorkId, + }, + }); + }; + + const handleCancel = () => { + resetForm(); + closeModal(); + }; + + const resetForm = () => { + methods.reset(); + }; + + const handleConfirmationChange = (e) => { + const value = e.target.value; + setConfirmationInput(value); + setConfirmationError(null); + }; + + return ( +
+
+ +
+
+
+

+ Transfer FileSets between Works +

+ +
+ +
+ {error && } + From Work ID: {fromWorkId} + + + + +
+ To execute this transfer, type "I understand" +
+
+ + {isSubmitted && confirmationError && ( +

+ {confirmationError.confirmationText} +

+ )} +
+
+
+ +
+ + +
+
+
+
+
+ ); +} + +WorkTabsPreservationTransferFileSetsModal.propTypes = { + closeModal: PropTypes.func, + isVisible: PropTypes.bool, + fromWorkId: PropTypes.string.isRequired, +}; + +export default WorkTabsPreservationTransferFileSetsModal; diff --git a/app/assets/js/components/Work/Tabs/Preservation/TransferFileSetsModal.test.jsx b/app/assets/js/components/Work/Tabs/Preservation/TransferFileSetsModal.test.jsx new file mode 100644 index 000000000..0ebbfbd2d --- /dev/null +++ b/app/assets/js/components/Work/Tabs/Preservation/TransferFileSetsModal.test.jsx @@ -0,0 +1,40 @@ +import React from "react"; +import TransferFileSetsModal from "./TransferFileSetsModal"; +import { + renderWithRouterApollo, + withReactHookForm, +} from "@js/services/testing-helpers"; +import { AuthProvider } from "@js/components/Auth/Auth"; +import { mockWork } from "@js/components/Work/work.gql.mock.js"; +import { screen } from "@testing-library/react"; +import { getCurrentUserMock } from "@js/components/Auth/auth.gql.mock"; + +let isModalOpen = true; + +const handleClose = () => { + isModalOpen = false; +}; + +describe("Transfer file sets to another work modal", () => { + beforeEach(() => { + const Wrapped = withReactHookForm(TransferFileSetsModal, { + closeModal: handleClose, + isVisible: isModalOpen, + fromWorkId: mockWork.id, + }); + return renderWithRouterApollo( + + + , + { + mocks: [ + getCurrentUserMock, + ], + } + ); + }); + + it("renders fileset form", async () => { + expect(await screen.findByTestId("transfer-filesets-form")); + }); +}); diff --git a/app/assets/js/components/Work/work.gql.js b/app/assets/js/components/Work/work.gql.js index 6ecadf677..06144bb9f 100644 --- a/app/assets/js/components/Work/work.gql.js +++ b/app/assets/js/components/Work/work.gql.js @@ -500,6 +500,14 @@ export const UPDATE_ACCESS_FILE_ORDER = gql` } `; +export const TRANSFER_FILE_SETS = gql` + mutation TransferFileSets($fromWorkId: ID!, $toWorkId: ID!) { + transferFileSets(fromWorkId: $fromWorkId, toWorkId: $toWorkId) { + id + } + } +`; + export const WORK_ARCHIVER_ENDPOINT = gql` query WorkArchiverEndpoint { workArchiverEndpoint { diff --git a/app/assets/js/components/Work/work.gql.mock.js b/app/assets/js/components/Work/work.gql.mock.js index 8b8107358..4adcfa033 100644 --- a/app/assets/js/components/Work/work.gql.mock.js +++ b/app/assets/js/components/Work/work.gql.mock.js @@ -4,6 +4,7 @@ import { GET_WORK, GET_WORK_TYPES, VERIFY_FILE_SETS, + TRANSFER_FILE_SETS, WORK_ARCHIVER_ENDPOINT, } from "@js/components/Work/work.gql.js"; import { mockVisibility, mockWorkType } from "@js/client-local"; @@ -11,6 +12,7 @@ import { mockVisibility, mockWorkType } from "@js/client-local"; import { GET_IIIF_MANIFEST_HEADERS } from "./work.gql"; export const MOCK_WORK_ID = "ABC123"; +export const MOCK_WORK_ID_2 = "DEF456"; export const mockWork = { id: MOCK_WORK_ID, @@ -501,6 +503,23 @@ export const deleteFilesetMock = { }, }; +export const transferFileSetsMock = { + request: { + query: TRANSFER_FILE_SETS, + variables: { + fromWorkId: MOCK_WORK_ID, + toWorkId: MOCK_WORK_ID_2 + }, + }, + result: { + data: { + transferFileSets: { + id: MOCK_WORK_ID, + }, + }, + }, +} + export const verifyFileSetsMock = { request: { query: VERIFY_FILE_SETS, diff --git a/app/lib/meadow/data.ex b/app/lib/meadow/data.ex index f412d3b04..60a6e9d68 100644 --- a/app/lib/meadow/data.ex +++ b/app/lib/meadow/data.ex @@ -27,6 +27,24 @@ defmodule Meadow.Data do |> Repo.one() end + @doc """ + Query returning a flattened list of FileSets for a Work grouped by rank. + + ## Examples + + iex> ranked_file_sets_for_work("01DT7V79D45B8BQMVS6YDRSF9J") + [%Meadow.Data.Schemas.FileSet{rank: -100}, %Meadow.Data.Schemas.FileSet{rank: 0}, %Meadow.Data.Schemas.FileSet{rank: 100}] + + iex> ranked_file_sets_for_work(Ecto.UUID.generate()) + [] + + """ + def ranked_file_sets_for_work(work_id) do + Enum.flat_map(["A", "P", "S", "X"], fn role -> + ranked_file_sets_for_work(work_id, role) + end) + end + @doc """ Query returning a list of FileSets for a Work ordered by `:rank`. diff --git a/app/lib/meadow/data/works/transfer_file_sets.ex b/app/lib/meadow/data/works/transfer_file_sets.ex new file mode 100644 index 000000000..53fd1179f --- /dev/null +++ b/app/lib/meadow/data/works/transfer_file_sets.ex @@ -0,0 +1,143 @@ +defmodule Meadow.Data.Works.TransferFileSets do + @moduledoc """ + Transfer file sets from one work to another. + """ + + import Ecto.Query, warn: false + alias Ecto.Multi + alias Meadow.Data + alias Meadow.Data.Schemas.FileSet + alias Meadow.Data.Works + alias Meadow.Repo + + require Logger + + @doc """ + Transfer file sets from one work to another. + + ## Examples + + iex> TransferFileSets.transfer(from_work_id, to_work_id) + {:ok, to_work_id} + + iex> TransferFileSets.transfer(from_work_id, to_work_id) + {:error, [failed_operation: :fetch_work, failed_value: :work_not_found]} + """ + @spec transfer(Ecto.UUID.t(), Ecto.UUID.t()) :: + {:ok, Ecto.UUID.t()} | {:error, any()} + def transfer(from_work_id, to_work_id) do + multi = + Multi.new() + |> Multi.run(:from_work, fn _repo, _changes -> fetch_work(from_work_id) end) + |> Multi.run(:to_work, fn _repo, _changes -> fetch_work(to_work_id) end) + |> Multi.run(:check_work_types, fn _repo, %{from_work: from_work, to_work: to_work} -> + check_work_types(from_work, to_work) + end) + |> Multi.run(:transfer_file_sets, fn _repo, _changes -> + transfer_file_sets(from_work_id, to_work_id) + end) + |> Multi.run(:delete_empty_work, fn _repo, _changes -> delete_empty_work(from_work_id) end) + |> Multi.run(:refetch_to_work, fn _repo, _changes -> fetch_work(to_work_id) end) + + case Repo.transaction(multi) do + {:ok, %{refetch_to_work: work}} -> + {:ok, work} + + {:error, failed_operation, failed_value, _changes_so_far} -> + error_message = humanize_error(failed_operation, failed_value) + {:error, error_message} + end + end + + defp fetch_work(work_id) do + case Works.get_work(work_id) do + nil -> {:error, :work_not_found} + work -> {:ok, work} + end + rescue + Ecto.Query.CastError -> {:error, :work_not_found} + end + + defp check_work_types(%{work_type: %{id: from_type}}, %{work_type: %{id: to_type}}) do + if from_type == to_type do + {:ok, :work_type_match} + else + {:error, :work_type_mismatch} + end + end + + defp transfer_file_sets(from_work_id, to_work_id) do + max_rank_in_target_work = + FileSet + |> where(work_id: ^to_work_id) + |> select([fs], max(fs.rank)) + |> Repo.one() || 0 + + file_sets = Data.ranked_file_sets_for_work(from_work_id) + + updates = + file_sets + |> Enum.with_index(max_rank_in_target_work + 1) + |> Enum.map(fn {file_set, new_rank} -> + changeset = FileSet.changeset(file_set, %{work_id: to_work_id, rank: new_rank}) + + case Repo.update(changeset) do + {:ok, _} -> {:ok, :transferred} + {:error, _} -> {:error, :transfer_failed} + end + end) + + if Enum.all?(updates, fn {:ok, _} -> true end) do + Logger.info( + "Transferred #{Enum.count(updates)} file sets from #{from_work_id} to #{to_work_id}" + ) + + {:ok, :transferred} + else + {:error, :transfer_failed} + end + end + + defp delete_empty_work(work_id) do + work = Works.with_file_sets(work_id) + + if Enum.empty?(work.file_sets) do + case Repo.delete(work) do + {:ok, _} -> + Logger.info("Deleted empty work #{work_id}") + {:ok, :deleted} + + _ -> + {:error, :delete_failed} + end + else + {:error, :work_not_found} + end + end + + defp humanize_error(failed_operation, failed_value) do + "#{describe_operation(failed_operation)}: #{describe_error(failed_value)}" + end + + defp describe_operation(operation) do + case operation do + :from_work -> "Fetching 'from' work" + :to_work -> "Fetching 'to' work" + :check_work_types -> "Checking work types" + :transfer_file_sets -> "Transferring file sets" + :delete_empty_work -> "Deleting empty work" + :refetch_to_work -> "Refetching work" + _ -> "Unknown operation" + end + end + + defp describe_error(error) do + case error do + :work_not_found -> "work not found (no changes were made)" + :work_type_mismatch -> "work types do not match (no changes were made)" + :transfer_failed -> "file sets transfer failed (no changes were made)" + :delete_failed -> "deletion failed (no changes were made)" + _ -> "unknown error (no changes were made)" + end + end +end diff --git a/app/lib/meadow_web/resolvers/data.ex b/app/lib/meadow_web/resolvers/data.ex index 0f7ee026b..f1f5d05ea 100644 --- a/app/lib/meadow_web/resolvers/data.ex +++ b/app/lib/meadow_web/resolvers/data.ex @@ -5,6 +5,7 @@ defmodule MeadowWeb.Resolvers.Data do """ alias Meadow.Pipeline alias Meadow.Data.{FileSets, Works} + alias Meadow.Data.Works.TransferFileSets alias Meadow.Utils.ChangesetErrors def works(_, args, _) do @@ -192,6 +193,13 @@ defmodule MeadowWeb.Resolvers.Data do {:ok, Works.verify_file_sets(work_id)} end + def transfer_file_sets(_, %{from_work_id: from_work_id, to_work_id: to_work_id}, _) do + case TransferFileSets.transfer(from_work_id, to_work_id) do + {:ok, to_work_id} -> {:ok, to_work_id} + {:error, reason} -> {:error, reason} + end + end + def iiif_manifest_headers(_, %{work_id: work_id}, _) do case Works.iiif_manifest_headers(work_id) do {:ok, headers} -> diff --git a/app/lib/meadow_web/schema/types/data/work_types.ex b/app/lib/meadow_web/schema/types/data/work_types.ex index af648c27a..144eb3984 100644 --- a/app/lib/meadow_web/schema/types/data/work_types.ex +++ b/app/lib/meadow_web/schema/types/data/work_types.ex @@ -108,6 +108,15 @@ defmodule MeadowWeb.Schema.Data.WorkTypes do middleware(Middleware.Authorize, "Editor") resolve(&Resolvers.Data.update_access_file_order/3) end + + @desc "Swap file sets from one work to another" + field :transfer_file_sets, :work do + arg(:from_work_id, non_null(:id)) + arg(:to_work_id, non_null(:id)) + middleware(Middleware.Authenticate) + middleware(Middleware.Authorize, "Editor") + resolve(&Resolvers.Data.transfer_file_sets/3) + end end @desc "A work object" diff --git a/app/test/gql/TransferFileSets.gql b/app/test/gql/TransferFileSets.gql new file mode 100644 index 000000000..f1a1b4e61 --- /dev/null +++ b/app/test/gql/TransferFileSets.gql @@ -0,0 +1,5 @@ +mutation ($fromWorkId: ID!, $toWorkId: ID!) { + transferFileSets(fromWorkId: $fromWorkId, toWorkId: $toWorkId) { + id + } +} \ No newline at end of file diff --git a/app/test/meadow/data/works/transfer_file_sets_test.exs b/app/test/meadow/data/works/transfer_file_sets_test.exs new file mode 100644 index 000000000..922a292a5 --- /dev/null +++ b/app/test/meadow/data/works/transfer_file_sets_test.exs @@ -0,0 +1,94 @@ +defmodule Meadow.Data.Works.TransferFileSetsTest do + use Meadow.AuthorityCase + use Meadow.DataCase + use Meadow.S3Case + + alias Meadow.Data.Schemas.Work + alias Meadow.Data.Works + alias Meadow.Data.Works.TransferFileSets + alias Meadow.Repo + + describe "transfer/2" do + setup do + from_work = + work_with_file_sets_fixture(5, %{}, %{ + core_metadata: %{ + original_filename: "From Work", + location: "From Work" + } + }) + + to_work = + work_with_file_sets_fixture(3, %{}, %{ + core_metadata: %{ + original_filename: "To Work", + location: "To Work" + } + }) + + {:ok, from_work_id: from_work.id, to_work_id: to_work.id} + end + + test "swaps all file sets from one work to another and deletes the empty work", %{ + from_work_id: from_work_id, + to_work_id: to_work_id + } do + assert {:ok, %Work{id: to_work_id}} = TransferFileSets.transfer(from_work_id, to_work_id) + refute Works.get_work(from_work_id) + + assert_rank_ordering_valid(to_work_id) + end + + test "does not allow transferring file sets from one work type to another" do + from_work = work_with_file_sets_fixture(1, %{work_type: %{id: "IMAGE", scheme: "work_type"}}) + from_work_id = from_work.id + to_work = work_with_file_sets_fixture(1, %{work_type: %{id: "AUDIO", scheme: "work_type"}}) + to_work_id = to_work.id + + assert {:error, "Checking work types: work types do not match (no changes were made)"} = TransferFileSets.transfer(from_work_id, to_work_id) + + reloaded_from_work = Works.get_work!(from_work_id) |> Repo.preload(:file_sets) + reloaded_to_work = Works.get_work!(to_work_id) |> Repo.preload(:file_sets) + + assert Enum.map(from_work.file_sets, & &1.id) == + Enum.map(reloaded_from_work.file_sets, & &1.id) + + assert Enum.map(to_work.file_sets, & &1.id) == Enum.map(reloaded_to_work.file_sets, & &1.id) + end + + test "handles work retrieval errors for 'from' work", %{to_work_id: to_work_id} do + from_work_id = Ecto.UUID.generate() + assert {:error, "Fetching 'from' work: work not found (no changes were made)"} = TransferFileSets.transfer(from_work_id, to_work_id) + end + + test "handles work retrieval errors for 'to' work", %{ + from_work_id: from_work_id, + + } do + to_work_id = Ecto.UUID.generate() + + assert {:error, "Fetching 'to' work: work not found (no changes were made)"} = TransferFileSets.transfer(from_work_id, to_work_id) + end + end + + defp assert_rank_ordering_valid(to_work_id) do + Enum.each(["A", "P", "S", "X"], fn role -> + file_sets = Works.with_file_sets(to_work_id, role).file_sets |> Enum.sort_by(& &1.rank) + + {to_work_file_sets, from_work_file_sets} = + Enum.split_with(file_sets, fn fs -> + fs.core_metadata.location == "To Work" + end) + + ordered_file_sets = to_work_file_sets ++ from_work_file_sets + + rank_ordering_valid = + ordered_file_sets + |> Enum.map(& &1.rank) + |> Enum.chunk_every(2, 1, :discard) + |> Enum.all?(fn [a, b] -> a < b end) + + assert rank_ordering_valid, "Rank ordering is not valid for role #{role}" + end) + end +end diff --git a/app/test/meadow/data/works_test.exs b/app/test/meadow/data/works_test.exs index 38bf8def5..65d1eb7b6 100644 --- a/app/test/meadow/data/works_test.exs +++ b/app/test/meadow/data/works_test.exs @@ -3,6 +3,8 @@ defmodule Meadow.Data.WorksTest do use Meadow.DataCase use Meadow.S3Case + import Assertions + alias Meadow.Config alias Meadow.Data.Schemas.Work alias Meadow.Data.{FileSets, Works} diff --git a/app/test/meadow_web/schema/mutation/transfer_file_sets_test.exs b/app/test/meadow_web/schema/mutation/transfer_file_sets_test.exs new file mode 100644 index 000000000..7ffb9e1f6 --- /dev/null +++ b/app/test/meadow_web/schema/mutation/transfer_file_sets_test.exs @@ -0,0 +1,75 @@ +defmodule MeadowWeb.Schema.Mutation.TransferFileSetsTest do + use Meadow.DataCase + use MeadowWeb.ConnCase, acync: true + use Wormwood.GQLCase + + load_gql(MeadowWeb.Schema, "test/gql/TransferFileSets.gql") + + describe "mutation" do + setup do + work1 = work_with_file_sets_fixture(3) + work2 = work_with_file_sets_fixture(3) + {:ok, %{work1: work1, work2: work2}} + end + + test "transfers file sets from one work to another", %{work1: work1, work2: work2} do + result = + query_gql( + variables: %{"fromWorkId" => work1.id, "toWorkId" => work2.id}, + context: gql_context() + ) + + assert {:ok, query_data} = result + end + + # test "missing IDs", %{work: work, ids: ids} do + # result = + # query_gql( + # variables: %{"workId" => work.id, "fileSetIds" => Enum.slice(ids, 0..2)}, + # context: gql_context() + # ) + + # assert {:ok, %{errors: [%{details: %{error: error_text}}]}} = result + + # Enum.slice(ids, 3..4) + # |> Enum.each(fn id -> + # assert String.contains?(error_text, id) + # end) + + # assert String.match?(error_text, ~r/missing \[.+\]/) + # end + + # test "extra IDs", %{work: work, ids: ids} do + # extra_ids = [Ecto.UUID.generate(), Ecto.UUID.generate()] + + # result = + # query_gql( + # variables: %{"workId" => work.id, "fileSetIds" => (ids ++ extra_ids) |> Enum.shuffle()}, + # context: gql_context() + # ) + + # assert {:ok, %{errors: [%{details: %{error: error_text}}]}} = result + + # Enum.each(extra_ids, fn id -> + # assert String.contains?(error_text, id) + # end) + + # assert String.match?(error_text, ~r/^Extra/) + # end + end + + describe "authorization" do + test "viewers are not authoried to update file set order" do + work = work_with_file_sets_fixture(1) + work2 = work_with_file_sets_fixture(1) + + result = + query_gql( + variables: %{"fromWorkId" => work.id, "toWorkId" => work2.id}, + context: %{current_user: %{role: "User"}} + ) + + assert {:ok, %{errors: [%{message: "Forbidden", status: 403}]}} = result + end + end +end diff --git a/app/test/support/test_helpers.ex b/app/test/support/test_helpers.ex index c6cd97825..5fdd992ed 100644 --- a/app/test/support/test_helpers.ex +++ b/app/test/support/test_helpers.ex @@ -195,7 +195,7 @@ defmodule Meadow.TestHelpers do def file_set_fixture_attrs(attrs \\ %{}) do Enum.into(attrs, %{ accession_number: attrs[:accession_number] || Faker.String.base64(), - role: attrs[:role] || %{id: Faker.Util.pick(["A", "P"]), scheme: "FILE_SET_ROLE"}, + role: attrs[:role] || %{id: Faker.Util.pick(["A", "P", "S", "X"]), scheme: "FILE_SET_ROLE"}, core_metadata: attrs[:core_metadata] || %{