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 (
+
+ );
+}
+
+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 cab9c942f..6127a1bfa 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
@@ -206,6 +207,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 2eadcff72..9cf0e1bdf 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 6e062daf7..7f0bf325b 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] ||
%{