zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

commit cfcbde418e34349e4a3ece2baa3d01c2b2da3376
parent 2c8deac62dbd767a4aa4bf99cedec63dc8a2c7f2
Author: srfsh <dev@srf.sh>
Date:   Tue, 16 Aug 2022 13:31:27 +0300

Zenflows{Test,}.VF.Proposal: init

Diffstat:
Msrc/zenflows/vf/proposal.ex | 8++++++++
Asrc/zenflows/vf/proposal/domain.ex | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/zenflows/vf/proposal/resolv.ex | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/vf/proposal/domain.test.exs | 164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/vf/proposal/type.test.exs | 214+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 581 insertions(+), 0 deletions(-)

diff --git a/src/zenflows/vf/proposal.ex b/src/zenflows/vf/proposal.ex @@ -23,6 +23,7 @@ Published requests or offers, sometimes with what is expected in return. use Zenflows.DB.Schema alias Zenflows.VF.{ + Intent, ProposedIntent, SpatialThing, Validate, @@ -35,6 +36,9 @@ alias Zenflows.VF.{ unit_based: boolean(), note: String.t() | nil, eligible_location: SpatialThing.t() | nil, + publishes: [ProposedIntent.t()], + primary_intents: [Intent.t()], + reciprocal_intents: [Intent.t()], } schema "vf_proposal" do @@ -47,6 +51,10 @@ schema "vf_proposal" do timestamps() has_many :publishes, ProposedIntent, foreign_key: :published_in_id + many_to_many :primary_intents, Intent, join_through: ProposedIntent, + join_keys: [published_in_id: :id, publishes_id: :id], join_where: [reciprocal: false] + many_to_many :reciprocal_intents, Intent, join_through: ProposedIntent, + join_keys: [published_in_id: :id, publishes_id: :id], join_where: [reciprocal: true] end @cast ~w[name has_beginning has_end unit_based note eligible_location_id]a diff --git a/src/zenflows/vf/proposal/domain.ex b/src/zenflows/vf/proposal/domain.ex @@ -0,0 +1,101 @@ +# Zenflows is designed to implement the Valueflows vocabulary, +# written and maintained by srfsh <info@dyne.org>. +# Copyright (C) 2021-2022 Dyne.org foundation <foundation@dyne.org>. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +defmodule Zenflows.VF.Proposal.Domain do +@moduledoc "Domain logic of Proposals." + +alias Ecto.Multi +alias Zenflows.DB.Repo +alias Zenflows.VF.Proposal + +@typep repo() :: Ecto.Repo.t() +@typep chgset() :: Ecto.Changeset.t() +@typep id() :: Zenflows.DB.Schema.id() +@typep params() :: Zenflows.DB.Schema.params() + +@spec one(repo(), id()) :: {:ok, Proposal.t()} | {:error, String.t()} +def one(repo \\ Repo, id) do + one_by(repo, id: id) +end + +@spec one_by(repo(), map() | Keyword.t()) + :: {:ok, Proposal.t()} | {:error, String.t()} +def one_by(repo \\ Repo, clauses) do + case repo.get_by(Proposal, clauses) do + nil -> {:error, "not found"} + found -> {:ok, found} + end +end + +@spec create(params()) :: {:ok, Proposal.t()} | {:error, chgset()} +def create(params) do + Multi.new() + |> Multi.insert(:insert, Proposal.chgset(params)) + |> Repo.transaction() + |> case do + {:ok, %{insert: p}} -> {:ok, p} + {:error, _, cset, _} -> {:error, cset} + end +end + +@spec update(id(), params()) :: + {:ok, Proposal.t()} | {:error, String.t() | chgset()} +def update(id, params) do + Multi.new() + |> Multi.put(:id, id) + |> Multi.run(:one, &one_by/2) + |> Multi.update(:update, &Proposal.chgset(&1.one, params)) + |> Repo.transaction() + |> case do + {:ok, %{update: p}} -> {:ok, p} + {:error, _, msg_or_cset, _} -> {:error, msg_or_cset} + end +end + +@spec delete(id()) + :: {:ok, Proposal.t()} | {:error, String.t() | chgset()} +def delete(id) do + Multi.new() + |> Multi.put(:id, id) + |> Multi.run(:one, &one_by/2) + |> Multi.delete(:delete, &(&1.one)) + |> Repo.transaction() + |> case do + {:ok, %{delete: pi}} -> {:ok, pi} + {:error, _, msg_or_cset, _} -> {:error, msg_or_cset} + end +end + +@spec preload(Proposal.t(), :eligible_location | :publishes + | :primary_intents | :reciprocal_intents) + :: Proposal.t() +def preload(prop, :eligible_location) do + Repo.preload(prop, :eligible_location) +end + +def preload(prop, :publishes) do + Repo.preload(prop, :publishes) +end + +def preload(prop, :primary_intents) do + Repo.preload(prop, :primary_intents) +end + +def preload(prop, :reciprocal_intents) do + Repo.preload(prop, :reciprocal_intents) +end +end diff --git a/src/zenflows/vf/proposal/resolv.ex b/src/zenflows/vf/proposal/resolv.ex @@ -0,0 +1,94 @@ +# Zenflows is designed to implement the Valueflows vocabulary, +# written and maintained by srfsh <info@dyne.org>. +# Copyright (C) 2021-2022 Dyne.org foundation <foundation@dyne.org>. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +defmodule Zenflows.VF.Proposal.Resolv do +@moduledoc "Resolvers of Proposal." + +alias Zenflows.VF.Proposal.Domain + +def eligible_location(prop, _, _) do + prop = Domain.preload(prop, :eligible_location) + {:ok, prop.eligible_location} +end + +def publishes(prop, _, _) do + prop = Domain.preload(prop, :publishes) + {:ok, prop.publishes} +end + +def primary_intents(prop, _, _) do + prop = Domain.preload(prop, :primary_intents) + {:ok, prop.primary_intents} +end + +def reciprocal_intents(prop, _, _) do + prop = Domain.preload(prop, :reciprocal_intents) + {:ok, prop.reciprocal_intents} +end + +def proposal(%{id: id}, _) do + Domain.one(id) +end + +def proposals(_params, _) do + {:ok, %{ + edges: [], + page_info: %{ + has_previous_page: false, + has_next_page: false, + }, + }} +end + +def offers(_params, _) do + {:ok, %{ + edges: [], + page_info: %{ + has_previous_page: false, + has_next_page: false, + }, + }} +end + +def requests(_params, _) do + {:ok, %{ + edges: [], + page_info: %{ + has_previous_page: false, + has_next_page: false, + }, + }} +end + +def create_proposal(%{proposal: params}, _) do + with {:ok, prop} <- Domain.create(params) do + {:ok, %{proposal: prop}} + end +end + +def update_proposal(%{proposal: %{id: id} = params}, _) do + with {:ok, prop} <- Domain.update(id, params) do + {:ok, %{proposal: prop}} + end +end + +def delete_proposal(%{id: id}, _) do + with {:ok, _} <- Domain.delete(id) do + {:ok, true} + end +end +end diff --git a/test/vf/proposal/domain.test.exs b/test/vf/proposal/domain.test.exs @@ -0,0 +1,164 @@ +# Zenflows is designed to implement the Valueflows vocabulary, +# written and maintained by srfsh <info@dyne.org>. +# Copyright (C) 2021-2022 Dyne.org foundation <foundation@dyne.org>. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +defmodule ZenflowsTest.VF.Proposal.Domain do +use ZenflowsTest.Help.EctoCase, async: true + +alias Zenflows.VF.{ + Proposal, + Proposal.Domain, + SpatialThing, +} + +setup do + %{ + params: %{ + name: Factory.str("name"), + note: Factory.str("note"), + has_beginning: Factory.now(), + has_end: Factory.now(), + unit_based: Factory.bool(), + eligible_location_id: Factory.insert!(:spatial_thing).id, + }, + inserted: Factory.insert!(:proposal), + id: Factory.id(), + } +end + +describe "one/1" do + test "with good id: finds the Proposal", %{inserted: %{id: id}} do + assert {:ok, %Proposal{}} = Domain.one(id) + end + + test "with bad id: doesn't find the Proposal", %{id: id} do + assert {:error, "not found"} = Domain.one(id) + end +end + +describe "create/1" do + test "with good params: creates a Proposal", %{params: params} do + assert {:ok, %Proposal{} = prop} = Domain.create(params) + assert prop.name == params.name + assert prop.note == params.note + assert prop.has_beginning == params.has_beginning + assert prop.has_end == params.has_end + assert prop.unit_based == params.unit_based + assert prop.eligible_location_id == params.eligible_location_id + end + + test "with empty params: creates a Proposal" do + assert {:ok, %Proposal{} = prop} = Domain.create(%{}) + assert prop.name == nil + assert prop.note == nil + assert prop.has_beginning == nil + assert prop.has_end == nil + assert prop.unit_based == false # since it defaults + assert prop.eligible_location_id == nil + end +end + +describe "update/2" do + test "with good params: updates the Proposal", %{params: params, inserted: old} do + assert {:ok, %Proposal{} = new} = Domain.update(old.id, params) + + assert new.name == params.name + assert new.note == params.note + assert new.has_beginning == params.has_beginning + assert new.has_end == params.has_end + assert new.unit_based == params.unit_based + assert new.eligible_location_id == params.eligible_location_id + end + + test "with bad params: doesn't update the Proposal", %{inserted: old} do + assert {:ok, %Proposal{} = new} = Domain.update(old.id, %{}) + + assert new.name == old.name + assert new.note == old.note + assert new.has_beginning == old.has_beginning + assert new.has_end == old.has_end + assert new.unit_based == old.unit_based + assert new.eligible_location_id == old.eligible_location_id + end +end + +describe "delete/1" do + test "with good id: deletes the Proposal", %{inserted: %{id: id}} do + assert {:ok, %Proposal{id: ^id}} = Domain.delete(id) + assert {:error, "not found"} = Domain.one(id) + end + + test "with bad id: doesn't delete the Proposal", %{id: id} do + assert {:error, "not found"} = Domain.delete(id) + end +end + +describe "preload/2" do + test "preloads `:eligible_location`", %{inserted: %{id: id}} do + assert {:ok, prop} = Domain.one(id) + prop = Domain.preload(prop, :eligible_location) + assert %SpatialThing{} = prop.eligible_location + end + + test "preloads `:publishes`", %{inserted: %{id: id}} do + assert {:ok, prop} = Domain.one(id) + + assert [] = Domain.preload(prop, :publishes).publishes + + left = + Enum.map(0..9, fn _ -> + Factory.insert!(:proposed_intent, %{published_in: prop}).id + end) + right = + Domain.preload(prop, :publishes) + |> Map.fetch!(:publishes) + |> Enum.map(& &1.id) + assert left -- right == [] + end + + test "preloads `:primary_intents`", %{inserted: %{id: id}} do + assert {:ok, prop} = Domain.one(id) + + assert [] = Domain.preload(prop, :primary_intents).primary_intents + + left = + Enum.map(0..9, fn _ -> + Factory.insert!(:proposed_intent, %{published_in: prop, reciprocal: false}).publishes_id + end) + right = + Domain.preload(prop, :primary_intents) + |> Map.fetch!(:primary_intents) + |> Enum.map(& &1.id) + assert left -- right == [] + end + + test "preloads `:reciprocal_intents`", %{inserted: %{id: id}} do + assert {:ok, prop} = Domain.one(id) + + assert [] = Domain.preload(prop, :reciprocal_intents).reciprocal_intents + + left = + Enum.map(0..9, fn _ -> + Factory.insert!(:proposed_intent, %{published_in: prop, reciprocal: true}).publishes_id + end) + right = + Domain.preload(prop, :reciprocal_intents) + |> Map.fetch!(:reciprocal_intents) + |> Enum.map(& &1.id) + assert left -- right == [] + end +end +end diff --git a/test/vf/proposal/type.test.exs b/test/vf/proposal/type.test.exs @@ -0,0 +1,214 @@ +# Zenflows is designed to implement the Valueflows vocabulary, +# written and maintained by srfsh <info@dyne.org>. +# Copyright (C) 2021-2022 Dyne.org foundation <foundation@dyne.org>. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +defmodule ZenflowsTest.VF.Proposal.Type do +use ZenflowsTest.Help.AbsinCase, async: true + +setup do + %{ + params: %{ + "name" => Factory.str("name"), + "note" => Factory.str("note"), + "hasBeginning" => Factory.iso_now(), + "hasEnd" => Factory.iso_now(), + "unitBased" => Factory.bool(), + "eligibleLocation" => Factory.insert!(:spatial_thing).id, + }, + inserted: Factory.insert!(:proposal), + } +end + +@frag """ +fragment proposal on Proposal { + id + name + note + hasBeginning + hasEnd + unitBased + eligibleLocation {id} +} +""" +describe "Query" do + test "proposal", %{inserted: prop} do + assert %{data: %{"proposal" => data}} = + run!(""" + #{@frag} + query ($id: ID!) { + proposal(id: $id) {...proposal} + } + """, vars: %{"id" => prop.id}) + + assert data["id"] == prop.id + assert data["name"] == prop.name + assert data["note"] == prop.note + assert data["unitBased"] == prop.unit_based + assert data["eligibleLocation"]["id"] == prop.eligible_location_id + assert {:ok, has_beginning, 0} = DateTime.from_iso8601(data["hasBeginning"]) + assert DateTime.compare(DateTime.utc_now(), has_beginning) != :lt + assert {:ok, has_end, 0} = DateTime.from_iso8601(data["hasEnd"]) + assert DateTime.compare(DateTime.utc_now(), has_end) != :lt + end + + test "proposals" do + assert %{data: %{"proposals" => data}} = + run!(""" + #{@frag} + query { + proposals { + pageInfo { + startCursor + endCursor + hasPreviousPage + hasNextPage + totalCount + pageLimit + } + edges { + cursor + node {...proposal} + } + } + } + """) + assert %{ + "pageInfo" => %{ + "startCursor" => nil, + "endCursor" => nil, + "hasPreviousPage" => false, + "hasNextPage" => false, + "totalCount" => nil, + "pageLimit" => nil, + }, + "edges" => [], + } = data + end + + test "offers" do + assert %{data: %{"offers" => data}} = + run!(""" + #{@frag} + query { + offers { + pageInfo { + startCursor + endCursor + hasPreviousPage + hasNextPage + totalCount + pageLimit + } + edges { + cursor + node {...proposal} + } + } + } + """) + assert %{ + "pageInfo" => %{ + "startCursor" => nil, + "endCursor" => nil, + "hasPreviousPage" => false, + "hasNextPage" => false, + "totalCount" => nil, + "pageLimit" => nil, + }, + "edges" => [], + } = data + end + + test "requests" do + assert %{data: %{"requests" => data}} = + run!(""" + #{@frag} + query { + requests { + pageInfo { + startCursor + endCursor + hasPreviousPage + hasNextPage + totalCount + pageLimit + } + edges { + cursor + node {...proposal} + } + } + } + """) + assert %{ + "pageInfo" => %{ + "startCursor" => nil, + "endCursor" => nil, + "hasPreviousPage" => false, + "hasNextPage" => false, + "totalCount" => nil, + "pageLimit" => nil, + }, + "edges" => [], + } = data + end +end + +describe "Mutation" do + test "createProposal", %{params: params} do + assert %{data: %{"createProposal" => %{"proposal" => data}}} = + run!(""" + #{@frag} + mutation ($proposal: ProposalCreateParams!) { + createProposal(proposal: $proposal) { + proposal {...proposal} + } + } + """, vars: %{"proposal" => params}) + + assert {:ok, _} = Zenflows.DB.ID.cast(data["id"]) + keys = ~w[name note unitBased hasBeginning hasEnd] + assert Map.take(data, keys) == Map.take(params, keys) + assert data["eligibleLocation"]["id"] == params["eligibleLocation"] + end + + test "updateProposal", %{params: params, inserted: prop} do + assert %{data: %{"updateProposal" => %{"proposal" => data}}} = + run!(""" + #{@frag} + mutation ($proposal: ProposalUpdateParams!) { + updateProposal(proposal: $proposal) { + proposal {...proposal} + } + } + """, vars: %{"proposal" => params |> Map.put("id", prop.id)}) + + assert data["id"] == prop.id + keys = ~w[name note unitBased hasBeginning hasEnd] + assert Map.take(data, keys) == Map.take(params, keys) + assert data["eligibleLocation"]["id"] == params["eligibleLocation"] + end + + test "deleteProposal", %{inserted: %{id: id}} do + assert %{data: %{"deleteProposal" => true}} = + run!(""" + mutation ($id: ID!) { + deleteProposal(id: $id) + } + """, vars: %{"id" => id}) + end +end +end