From 11da4e1fbfaaa0e36f8a9c0186fdaebddb5af9ba Mon Sep 17 00:00:00 2001 From: Josh Smith Date: Mon, 27 Nov 2017 13:18:38 -0800 Subject: [PATCH] Improve organization creation - Add Organizations modules and fix changesets, add migration - Changed fulfillment behavior to be code based - Updated API docs - Added explicit flagging as unapproved when creating organization - Add test coverage for slug generation behavior --- blueprint/api.apib | 23 +++++++--- lib/code_corps/model/organization.ex | 26 ++++++++--- lib/code_corps/model/organization_invite.ex | 21 +++++---- lib/code_corps/organizations/organizations.ex | 41 +++++++++++++++++ lib/code_corps/policy/helpers.ex | 10 ----- lib/code_corps/policy/organization.ex | 32 ++++++++++--- .../controllers/organization_controller.ex | 4 +- .../organization_invite_controller.ex | 2 - lib/code_corps_web/views/changeset_view.ex | 6 ++- .../views/organization_invite_view.ex | 4 +- priv/gettext/en/LC_MESSAGES/errors.po | 12 ++++- priv/gettext/errors.pot | 12 ++++- ...change_organization_invite_fulfillment.exs | 21 +++++++++ priv/repo/structure.sql | 25 ++++++++--- .../model/organization_invite_test.exs | 33 +++++--------- .../code_corps/model/organization_test.exs | 23 +++++++++- .../organizations/organizations_test.exs | 45 +++++++++++++++++++ .../code_corps/policy/organization_test.exs | 13 +++--- .../organization_controller_test.exs | 29 +++++++++--- .../views/changeset_view_test.exs | 2 +- .../views/organization_invite_view_test.exs | 12 ++++- test/support/factories.ex | 1 - 22 files changed, 302 insertions(+), 95 deletions(-) create mode 100644 lib/code_corps/organizations/organizations.ex create mode 100644 priv/repo/migrations/20171127215847_change_organization_invite_fulfillment.exs create mode 100644 test/lib/code_corps/organizations/organizations_test.exs diff --git a/blueprint/api.apib b/blueprint/api.apib index f996c1a11..542c0790a 100644 --- a/blueprint/api.apib +++ b/blueprint/api.apib @@ -507,13 +507,11 @@ Note that a Github Repo relates 1:1 to a Github App Installation, since only a s This endpoint is for Creating, Updating, returning Organization Invites on Code Corps. -`fulfilled` field may only change from false to true. - ##Organization Invites [/organization-invites] ### Create an organization invite [POST] -Admin creates new organization invites which contains `email` and `title`. A `code` is automatically generated on creation and `fulfilled` is set to false. +Admin creates new organization invites which contains `email` and `title`. A `code` is automatically generated on creation. On successful creation an email is sent to the given `email` address. @@ -614,7 +612,7 @@ On successful creation an email is sent to the given `email` address. This endpoint retrieves Organizations on Code Corps. Organizations usually have one or more Projects. -Until the Code Corps platform is open to new organizations, only admin users can create new organizations. +A valid `invite_code` parameter is required to create an Organization. Without this parameter, only admin users are allowed to create new organizations. ## Organizations [/organizations] @@ -626,6 +624,7 @@ Until the Code Corps platform is open to new organizations, only admin users can Accept: application/vnd.api+json Authorization: Bearer + + Attributes (Organization Create Request) + Response 201 (application/vnd.api+json; charset=utf-8) @@ -3035,10 +3034,23 @@ The platform stores Stripe customers and cards so they can be reused across diff + `icon-large-url`: `//res.cloudinary.com/dlfnmtoq1/image/upload/c_fill,h_500,w_500/pp0md2banaw7k6oa1ew9` + `icon-thumb-url`: `//res.cloudinary.com/dlfnmtoq1/image/upload/c_fill,h_100,w_100/pp0md2banaw7k6oa1ew9` + `inserted-at`: `2016-07-08T03:03:51.967Z` (string) ++ `invite-code`: `valid-invite-code` (string) + name: `Code Corps` -+ slug: `code_corps` ++ slug: `code-corps` + `updated-at`: `2016-07-08T03:03:51.967Z` (string) +## Organization Create Request (object) ++ type: `organization` (string, required) ++ attributes + + `cloudinary-public-id`: `pp0md2banaw7k6oa1ew9` (string, required) + + description: `Build a better future.` (string, required) + + `invite-code`: `valid-invite-code` (string) + + name: `Code Corps` (string, required) + + slug: `code-corps` (string) ++ relationships + + owner + + data(User Resource Identifier) + ## Organization Resource (object) + include Organization Resource Identifier + attributes(Organization Attributes) @@ -3069,7 +3081,6 @@ The platform stores Stripe customers and cards so they can be reused across diff + email: `head@corecorps.com` (string) + `inserted-at`: `2016-07-08T03:03:51.967Z` (string) + `updated-at`: `2016-07-08T03:03:51.967Z` (string) -+ fulfilled: `false` (boolean) ## Organization Invite Resource (object) + include Organization Invite Resource Identifier diff --git a/lib/code_corps/model/organization.ex b/lib/code_corps/model/organization.ex index b8a6fc28f..192ef9317 100644 --- a/lib/code_corps/model/organization.ex +++ b/lib/code_corps/model/organization.ex @@ -10,19 +10,22 @@ defmodule CodeCorps.Organization do import CodeCorps.Validators.SlugValidator alias CodeCorps.SluggedRoute + alias Ecto.Changeset @type t :: %__MODULE__{} schema "organizations" do + field :approved, :boolean field :cloudinary_public_id field :default_color field :description, :string + field :invite_code, :string, virtual: true field :name, :string field :slug, :string - field :approved, :boolean belongs_to :owner, CodeCorps.User + has_one :organization_invite, CodeCorps.OrganizationInvite has_one :slugged_route, CodeCorps.SluggedRoute has_one :stripe_connect_account, CodeCorps.StripeConnectAccount @@ -37,8 +40,8 @@ defmodule CodeCorps.Organization do """ def changeset(struct, params \\ %{}) do struct - |> cast(params, [:name, :description, :slug, :cloudinary_public_id, :default_color]) - |> validate_required([:name, :description]) + |> cast(params, [:cloudinary_public_id, :description, :default_color, :name, :slug]) + |> validate_required([:description, :name]) end @doc """ @@ -47,16 +50,25 @@ defmodule CodeCorps.Organization do def create_changeset(struct, params) do struct |> changeset(params) - |> cast(params, [:owner_id]) - |> generate_slug(:name, :slug) - |> validate_required([:description, :owner_id, :slug]) + |> cast(params, [:invite_code, :owner_id]) + |> maybe_generate_slug() + |> validate_required([:cloudinary_public_id, :description, :owner_id, :slug]) |> assoc_constraint(:owner) |> validate_slug(:slug) + |> unique_constraint(:slug, name: :organizations_lower_slug_index) |> put_slugged_route() |> generate_icon_color(:default_color) + |> put_change(:approved, false) + end + + defp maybe_generate_slug(%Changeset{changes: %{slug: _}} = changeset) do + changeset + end + defp maybe_generate_slug(%Changeset{} = changeset) do + changeset |> generate_slug(:name, :slug) end - defp put_slugged_route(changeset) do + defp put_slugged_route(%Changeset{} = changeset) do case changeset do %Ecto.Changeset{valid?: true, changes: %{slug: slug}} -> slugged_route_changeset = SluggedRoute.create_changeset(%SluggedRoute{}, %{slug: slug}) diff --git a/lib/code_corps/model/organization_invite.ex b/lib/code_corps/model/organization_invite.ex index 0cfe59624..f3f98a21d 100644 --- a/lib/code_corps/model/organization_invite.ex +++ b/lib/code_corps/model/organization_invite.ex @@ -10,9 +10,10 @@ defmodule CodeCorps.OrganizationInvite do schema "organization_invites" do field :code, :string field :email, :string - field :fulfilled, :boolean, default: false field :organization_name, :string + belongs_to :organization, CodeCorps.Organization + timestamps() end @@ -21,10 +22,9 @@ defmodule CodeCorps.OrganizationInvite do """ def changeset(struct, params \\ %{}) do struct - |> cast(params, [:email, :organization_name, :fulfilled]) + |> cast(params, [:email, :organization_name]) |> validate_required([:email, :organization_name]) |> validate_format(:email, ~r/@/) - |> validate_change(:fulfilled, &check_fulfilled_changes_to_true/2) end @doc """ @@ -37,6 +37,13 @@ defmodule CodeCorps.OrganizationInvite do |> unique_constraint(:code) end + def update_changeset(struct, params) do + struct + |> changeset(params) + |> cast(params, [:organization_id]) + |> assoc_constraint(:organization) + end + defp generate_code(changeset) do case changeset do %Ecto.Changeset{valid?: true} -> @@ -52,12 +59,4 @@ defmodule CodeCorps.OrganizationInvite do |> Base.encode64 |> binary_part(0, length) end - - defp check_fulfilled_changes_to_true :fulfilled, fulfilled do - if fulfilled == false do - [fulfillled: "Fulfilled can only change from false to true"] - else - [] - end - end end diff --git a/lib/code_corps/organizations/organizations.ex b/lib/code_corps/organizations/organizations.ex new file mode 100644 index 000000000..cc3b5f34c --- /dev/null +++ b/lib/code_corps/organizations/organizations.ex @@ -0,0 +1,41 @@ +defmodule CodeCorps.Organizations do + @moduledoc ~S""" + """ + + alias CodeCorps.{Organization, OrganizationInvite, Repo} + alias Ecto.{Changeset, Multi} + + @doc ~S""" + Creates a `CodeCorps.Organization` from a set of parameters, + fulfilling the associated `CodeCorps.OrganizationInvite`, if it exists, by + associating it with the created record. + """ + @spec create(map) :: {:ok, Organization.t} | {:error, Changeset.t} + def create(%{} = params) do + Multi.new() + |> Multi.insert(:organization, Organization.create_changeset(%Organization{}, params)) + |> Multi.run(:organization_invite, fn %{organization: organization} -> organization |> fulfill_associated_invite(params) end) + |> Repo.transaction() + |> handle_result() + end + + @spec fulfill_associated_invite(Organization.t, map) :: {:ok, OrganizationInvite.t | nil} | {:error, Changeset.t} + defp fulfill_associated_invite(%Organization{id: organization_id}, %{"invite_code" => code}) do + OrganizationInvite + |> Repo.get_by(code: code) + |> OrganizationInvite.update_changeset(%{organization_id: organization_id}) + |> Repo.update() + end + defp fulfill_associated_invite(%Organization{}, %{}), do: {:ok, nil} + + @spec handle_result(tuple) :: tuple + defp handle_result({:ok, %{organization: %Organization{} = organization}}) do + {:ok, organization} + end + defp handle_result({:error, :organization, %Changeset{} = changeset, _steps}) do + {:error, changeset} + end + defp handle_result({:error, :organization_invite, %Changeset{} = changeset, _steps}) do + {:error, changeset} + end +end diff --git a/lib/code_corps/policy/helpers.ex b/lib/code_corps/policy/helpers.ex index f45a1cdf7..c27b0e459 100644 --- a/lib/code_corps/policy/helpers.ex +++ b/lib/code_corps/policy/helpers.ex @@ -6,7 +6,6 @@ defmodule CodeCorps.Policy.Helpers do alias CodeCorps.{ Organization, - OrganizationInvite, ProjectUser, Project, ProjectUser, @@ -67,15 +66,6 @@ defmodule CodeCorps.Policy.Helpers do def get_organization(_), do: nil - @doc """ - Retrieves an organiation invite from a struct, containing a `code` field - Returns `CodeCorps.OrganizationInvite` or nil - """ - @spec get_organization_invite(struct) :: OrganizationInvite.t() | nil - def get_organization_invite(%{"code" => code}), - do: OrganizationInvite |> Repo.get_by(code: code, fulfilled: false) - def get_organization_invite(%{}), do: nil - @doc """ Retrieves a project record, from a model struct, or an `Ecto.Changeset` containing a `project_id` field diff --git a/lib/code_corps/policy/organization.ex b/lib/code_corps/policy/organization.ex index 810c22f06..94bc57a8f 100644 --- a/lib/code_corps/policy/organization.ex +++ b/lib/code_corps/policy/organization.ex @@ -2,20 +2,38 @@ defmodule CodeCorps.Policy.Organization do @moduledoc ~S""" Authorization policies for performing actions on `Organization` records """ - import CodeCorps.Policy.Helpers, - only: [owned_by?: 2, get_organization_invite: 1] + import CodeCorps.Policy.Helpers, only: [owned_by?: 2] - alias CodeCorps.{Organization, User} + import Ecto.Query + alias CodeCorps.{Organization, OrganizationInvite, Repo, User} + + @doc ~S""" + Returns a boolean indicating if the specified user is allowed to create the + organization specified by a map of attributes. + """ + @spec create?(User.t, map) :: boolean def create?(%User{admin: true}, %{}), do: true - def create?(%User{}, %{} = params) do - case get_organization_invite(params) do + def create?(%User{}, %{"invite_code" => invite_code}) do + case invite_code |> get_invite() do nil -> false - _ -> true + _invite -> true end end - def create?(%{}, %{}), do: false + def create?(%User{}, %{}), do: false + @doc ~S""" + Returns a boolean indicating if the specified user is allowed to update the + specified organization. + """ + @spec update?(User.t, Organization.t) :: boolean def update?(%User{admin: true}, %Organization{}), do: true def update?(%User{} = user, %Organization{} = organization), do: organization |> owned_by?(user) + + @spec get_invite(String.t) :: OrganizationInvite.t | nil + defp get_invite(code) do + OrganizationInvite + |> where([oi], is_nil(oi.organization_id)) + |> Repo.get_by(code: code) + end end diff --git a/lib/code_corps_web/controllers/organization_controller.ex b/lib/code_corps_web/controllers/organization_controller.ex index 23f1b74a4..bb36930fe 100644 --- a/lib/code_corps_web/controllers/organization_controller.ex +++ b/lib/code_corps_web/controllers/organization_controller.ex @@ -2,7 +2,7 @@ defmodule CodeCorpsWeb.OrganizationController do @moduledoc false use CodeCorpsWeb, :controller - alias CodeCorps.{Helpers.Query, Organization, User} + alias CodeCorps.{Helpers.Query, Organization, Organizations, User} action_fallback CodeCorpsWeb.FallbackController plug CodeCorpsWeb.Plug.DataToAttributes @@ -30,7 +30,7 @@ defmodule CodeCorpsWeb.OrganizationController do def create(%Conn{} = conn, %{} = params) do with %User{} = current_user <- conn |> CodeCorps.Guardian.Plug.current_resource, {:ok, :authorized} <- current_user |> Policy.authorize(:create, %Organization{}, params), - {:ok, %Organization{} = organization} <- %Organization{} |> Organization.create_changeset(params) |> Repo.insert, + {:ok, %Organization{} = organization} <- Organizations.create(params), organization <- preload(organization) do conn |> put_status(:created) |> render("show.json-api", data: organization) diff --git a/lib/code_corps_web/controllers/organization_invite_controller.ex b/lib/code_corps_web/controllers/organization_invite_controller.ex index 6d15e92c3..503002d4e 100644 --- a/lib/code_corps_web/controllers/organization_invite_controller.ex +++ b/lib/code_corps_web/controllers/organization_invite_controller.ex @@ -4,8 +4,6 @@ defmodule CodeCorpsWeb.OrganizationInviteController do alias CodeCorps.{Emails, Helpers.Query, Mailer, OrganizationInvite, User} - import Ecto.Query - action_fallback CodeCorpsWeb.FallbackController plug CodeCorpsWeb.Plug.DataToAttributes plug CodeCorpsWeb.Plug.IdsToIntegers diff --git a/lib/code_corps_web/views/changeset_view.ex b/lib/code_corps_web/views/changeset_view.ex index c069d49c5..a64183a44 100644 --- a/lib/code_corps_web/views/changeset_view.ex +++ b/lib/code_corps_web/views/changeset_view.ex @@ -32,7 +32,7 @@ defmodule CodeCorpsWeb.ChangesetView do defp format_attribute_errors(errors, attribute) do errors |> Map.get(attribute) - |> Enum.map(fn(message) -> create_error(attribute, message) end) + |> Enum.map(&create_error(attribute, &1)) end def create_error(attribute, message) do @@ -40,7 +40,7 @@ defmodule CodeCorpsWeb.ChangesetView do detail: format_detail(attribute, message), title: message, source: %{ - pointer: "data/attributes/#{attribute}" + pointer: "data/attributes/#{Utils.format_key(attribute)}" }, status: "422" } @@ -61,6 +61,8 @@ defmodule CodeCorpsWeb.ChangesetView do "#{attribute |> Utils.humanize |> translate_attribute} #{message}" end + defp translate_attribute("Cloudinary public"), do: dgettext("errors", "Cloudinary public") defp translate_attribute("Github"), do: dgettext("errors", "Github") + defp translate_attribute("Slug"), do: dgettext("errors", "Slug") defp translate_attribute(attribute), do: attribute end diff --git a/lib/code_corps_web/views/organization_invite_view.ex b/lib/code_corps_web/views/organization_invite_view.ex index 424c26b21..971ed6724 100644 --- a/lib/code_corps_web/views/organization_invite_view.ex +++ b/lib/code_corps_web/views/organization_invite_view.ex @@ -4,6 +4,8 @@ defmodule CodeCorpsWeb.OrganizationInviteView do use JaSerializer.PhoenixView attributes [ - :email, :fulfilled, :inserted_at, :organization_name, :updated_at + :email, :inserted_at, :organization_name, :updated_at ] + + has_one :organization, type: "organization", field: :organization_id end diff --git a/priv/gettext/en/LC_MESSAGES/errors.po b/priv/gettext/en/LC_MESSAGES/errors.po index b778b127c..5a94f0029 100644 --- a/priv/gettext/en/LC_MESSAGES/errors.po +++ b/priv/gettext/en/LC_MESSAGES/errors.po @@ -96,10 +96,18 @@ msgstr "" msgid "must be equal to %{number}" msgstr "" -#: lib/code_corps/accounts/changesets.ex:50 +#: lib/code_corps/accounts/changesets.ex:71 msgid "account is already connected to someone else" msgstr "" -#: lib/code_corps_web/views/changeset_view.ex:63 +#: lib/code_corps_web/views/changeset_view.ex:65 msgid "Github" msgstr "GitHub" + +#: lib/code_corps_web/views/changeset_view.ex:66 +msgid "Slug" +msgstr "Vanity URL" + +#: lib/code_corps_web/views/changeset_view.ex:64 +msgid "Cloudinary public" +msgstr "Image" diff --git a/priv/gettext/errors.pot b/priv/gettext/errors.pot index 1b9b53fa6..5f9449429 100644 --- a/priv/gettext/errors.pot +++ b/priv/gettext/errors.pot @@ -93,10 +93,18 @@ msgstr "" msgid "must be equal to %{number}" msgstr "" -#: lib/code_corps/accounts/changesets.ex:50 +#: lib/code_corps/accounts/changesets.ex:71 msgid "account is already connected to someone else" msgstr "" -#: lib/code_corps_web/views/changeset_view.ex:63 +#: lib/code_corps_web/views/changeset_view.ex:65 msgid "Github" msgstr "" + +#: lib/code_corps_web/views/changeset_view.ex:66 +msgid "Slug" +msgstr "" + +#: lib/code_corps_web/views/changeset_view.ex:64 +msgid "Cloudinary public" +msgstr "" diff --git a/priv/repo/migrations/20171127215847_change_organization_invite_fulfillment.exs b/priv/repo/migrations/20171127215847_change_organization_invite_fulfillment.exs new file mode 100644 index 000000000..723d60142 --- /dev/null +++ b/priv/repo/migrations/20171127215847_change_organization_invite_fulfillment.exs @@ -0,0 +1,21 @@ +defmodule CodeCorps.Repo.Migrations.ChangeOrganizationInviteFulfillment do + use Ecto.Migration + + def up do + alter table(:organization_invites) do + add :organization_id, references(:organizations, on_delete: :nothing) + remove :fulfilled + end + + create index(:organization_invites, [:organization_id], unique: true) + end + + def down do + drop_if_exists index(:organization_invites, [:organization_id], unique: true) + + alter table(:organization_invites) do + remove :organization_id + add :fulfilled, :boolean, default: false + end + end +end diff --git a/priv/repo/structure.sql b/priv/repo/structure.sql index 3c855d1a4..4406e0f4a 100644 --- a/priv/repo/structure.sql +++ b/priv/repo/structure.sql @@ -2,8 +2,8 @@ -- PostgreSQL database dump -- --- Dumped from database version 9.5.10 --- Dumped by pg_dump version 10.1 +-- Dumped from database version 10.0 +-- Dumped by pg_dump version 10.0 SET statement_timeout = 0; SET lock_timeout = 0; @@ -567,9 +567,9 @@ CREATE TABLE organization_invites ( code character varying(255) NOT NULL, email character varying(255) NOT NULL, organization_name character varying(255) NOT NULL, - fulfilled boolean DEFAULT false NOT NULL, inserted_at timestamp without time zone NOT NULL, - updated_at timestamp without time zone NOT NULL + updated_at timestamp without time zone NOT NULL, + organization_id bigint ); @@ -2669,6 +2669,13 @@ CREATE UNIQUE INDEX organization_invites_code_index ON organization_invites USIN CREATE INDEX organization_invites_email_index ON organization_invites USING btree (email); +-- +-- Name: organization_invites_organization_id_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX organization_invites_organization_id_index ON organization_invites USING btree (organization_id); + + -- -- Name: organizations_approved_index; Type: INDEX; Schema: public; Owner: - -- @@ -3495,6 +3502,14 @@ ALTER TABLE ONLY organization_github_app_installations ADD CONSTRAINT organization_github_app_installations_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id); +-- +-- Name: organization_invites organization_invites_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY organization_invites + ADD CONSTRAINT organization_invites_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id); + + -- -- Name: organizations organizations_owner_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -3883,5 +3898,5 @@ ALTER TABLE ONLY users -- PostgreSQL database dump complete -- -INSERT INTO "schema_migrations" (version) VALUES (20160723215749), (20160804000000), (20160804001111), (20160805132301), (20160805203929), (20160808143454), (20160809214736), (20160810124357), (20160815125009), (20160815143002), (20160816020347), (20160816034021), (20160817220118), (20160818000944), (20160818132546), (20160820113856), (20160820164905), (20160822002438), (20160822004056), (20160822011624), (20160822020401), (20160822044612), (20160830081224), (20160830224802), (20160911233738), (20160912002705), (20160912145957), (20160918003206), (20160928232404), (20161003185918), (20161019090945), (20161019110737), (20161020144622), (20161021131026), (20161031001615), (20161121005339), (20161121014050), (20161121043941), (20161121045709), (20161122015942), (20161123081114), (20161123150943), (20161124085742), (20161125200620), (20161126045705), (20161127054559), (20161205024856), (20161207112519), (20161209192504), (20161212005641), (20161214005935), (20161215052051), (20161216051447), (20161218005913), (20161219160401), (20161219163909), (20161220141753), (20161221085759), (20161226213600), (20161231063614), (20170102130055), (20170102181053), (20170104113708), (20170104212623), (20170104235423), (20170106013143), (20170115035159), (20170115230549), (20170121014100), (20170131234029), (20170201014901), (20170201025454), (20170201035458), (20170201183258), (20170220032224), (20170224233516), (20170226050552), (20170228085250), (20170308214128), (20170308220713), (20170308222552), (20170313130611), (20170318032449), (20170318082740), (20170324194827), (20170424215355), (20170501225441), (20170505224222), (20170526095401), (20170602000208), (20170622205732), (20170626231059), (20170628092119), (20170628213609), (20170629183404), (20170630140136), (20170706132431), (20170707213648), (20170711122252), (20170717092127), (20170725060612), (20170727052644), (20170731130121), (20170814131722), (20170913114958), (20170921014405), (20170925214512), (20170925230419), (20170926134646), (20170927100300), (20170928234412), (20171003134956), (20171003225853), (20171006063358), (20171006161407), (20171012215106), (20171012221231), (20171016125229), (20171016125516), (20171016223356), (20171016235656), (20171017235433), (20171019191035), (20171025184225), (20171026010933), (20171027061833), (20171028011642), (20171028173508), (20171030182857), (20171031232023), (20171031234356), (20171101023309), (20171104013543), (20171106045740), (20171106050209), (20171106103153), (20171106200036), (20171109231538), (20171110001134), (20171114010851), (20171114033357), (20171114225214), (20171114225713), (20171114232534), (20171115201624), (20171115225358), (20171119004204), (20171121075226), (20171121144138), (20171123065902); +INSERT INTO "schema_migrations" (version) VALUES (20160723215749), (20160804000000), (20160804001111), (20160805132301), (20160805203929), (20160808143454), (20160809214736), (20160810124357), (20160815125009), (20160815143002), (20160816020347), (20160816034021), (20160817220118), (20160818000944), (20160818132546), (20160820113856), (20160820164905), (20160822002438), (20160822004056), (20160822011624), (20160822020401), (20160822044612), (20160830081224), (20160830224802), (20160911233738), (20160912002705), (20160912145957), (20160918003206), (20160928232404), (20161003185918), (20161019090945), (20161019110737), (20161020144622), (20161021131026), (20161031001615), (20161121005339), (20161121014050), (20161121043941), (20161121045709), (20161122015942), (20161123081114), (20161123150943), (20161124085742), (20161125200620), (20161126045705), (20161127054559), (20161205024856), (20161207112519), (20161209192504), (20161212005641), (20161214005935), (20161215052051), (20161216051447), (20161218005913), (20161219160401), (20161219163909), (20161220141753), (20161221085759), (20161226213600), (20161231063614), (20170102130055), (20170102181053), (20170104113708), (20170104212623), (20170104235423), (20170106013143), (20170115035159), (20170115230549), (20170121014100), (20170131234029), (20170201014901), (20170201025454), (20170201035458), (20170201183258), (20170220032224), (20170224233516), (20170226050552), (20170228085250), (20170308214128), (20170308220713), (20170308222552), (20170313130611), (20170318032449), (20170318082740), (20170324194827), (20170424215355), (20170501225441), (20170505224222), (20170526095401), (20170602000208), (20170622205732), (20170626231059), (20170628092119), (20170628213609), (20170629183404), (20170630140136), (20170706132431), (20170707213648), (20170711122252), (20170717092127), (20170725060612), (20170727052644), (20170731130121), (20170814131722), (20170913114958), (20170921014405), (20170925214512), (20170925230419), (20170926134646), (20170927100300), (20170928234412), (20171003134956), (20171003225853), (20171006063358), (20171006161407), (20171012215106), (20171012221231), (20171016125229), (20171016125516), (20171016223356), (20171016235656), (20171017235433), (20171019191035), (20171025184225), (20171026010933), (20171027061833), (20171028011642), (20171028173508), (20171030182857), (20171031232023), (20171031234356), (20171101023309), (20171104013543), (20171106045740), (20171106050209), (20171106103153), (20171106200036), (20171109231538), (20171110001134), (20171114010851), (20171114033357), (20171114225214), (20171114225713), (20171114232534), (20171115201624), (20171115225358), (20171119004204), (20171121075226), (20171121144138), (20171123065902), (20171127215847); diff --git a/test/lib/code_corps/model/organization_invite_test.exs b/test/lib/code_corps/model/organization_invite_test.exs index 4ea3cd511..b44a3e95d 100644 --- a/test/lib/code_corps/model/organization_invite_test.exs +++ b/test/lib/code_corps/model/organization_invite_test.exs @@ -16,31 +16,22 @@ defmodule CodeCorps.OrganizationInviteTest do changeset = OrganizationInvite.changeset(%OrganizationInvite{}, @invalid_attrs) refute changeset.valid? end + end - test "field fulfilled changes only from false to true" do - changeset = OrganizationInvite.changeset(%OrganizationInvite{fulfilled: false}, Map.put(@valid_attrs, :fulfilled, true)) + describe "create_changeset/2" do + test "with valid attributes" do + changeset = OrganizationInvite.create_changeset(%OrganizationInvite{}, @valid_attrs) assert changeset.valid? + end - changeset = OrganizationInvite.changeset(%OrganizationInvite{fulfilled: true}, Map.put(@valid_attrs, :fulfilled, false)) + test "with invalid attributes" do + changeset = OrganizationInvite.create_changeset(%OrganizationInvite{}, @invalid_attrs) refute changeset.valid? end - end - - describe "create_changeset/2" do - test "with valid attributes" do - changeset = OrganizationInvite.create_changeset(%OrganizationInvite{}, @valid_attrs) - assert changeset.valid? - end - - test "with invalid attributes" do - changeset = OrganizationInvite.create_changeset(%OrganizationInvite{}, @invalid_attrs) - refute changeset.valid? - end - - test "generates code" do - changeset = OrganizationInvite.create_changeset(%OrganizationInvite{}, @valid_attrs) - assert changeset.changes.code != nil - end - end + test "generates code" do + changeset = OrganizationInvite.create_changeset(%OrganizationInvite{}, @valid_attrs) + assert changeset.changes.code != nil + end + end end diff --git a/test/lib/code_corps/model/organization_test.exs b/test/lib/code_corps/model/organization_test.exs index 1f44582c0..30a18636e 100644 --- a/test/lib/code_corps/model/organization_test.exs +++ b/test/lib/code_corps/model/organization_test.exs @@ -19,7 +19,12 @@ defmodule CodeCorps.OrganizationTest do end describe "create_changeset" do - @valid_attrs %{owner_id: 1, description: "Building a better future.", name: "Code Corps"} + @valid_attrs %{ + cloudinary_public_id: "foo", + description: "Building a better future.", + name: "Code Corps", + owner_id: 1 + } @invalid_attrs %{} test "with valid attributes" do @@ -41,5 +46,21 @@ defmodule CodeCorps.OrganizationTest do assert result == :error changeset |> assert_error_message(:owner, "does not exist") end + + test "sets approved to false" do + changeset = Organization.create_changeset(%Organization{}, @valid_attrs) + assert changeset |> get_field(:approved) == false + end + + test "generates slug if none provided" do + changeset = Organization.create_changeset(%Organization{}, @valid_attrs) + assert changeset |> get_field(:slug) == "code-corps" + end + + test "leaves out slug generation if slug is provided" do + attrs = @valid_attrs |> Map.put(:slug, "custom-slug") + changeset = Organization.create_changeset(%Organization{}, attrs) + assert changeset |> get_field(:slug) == "custom-slug" + end end end diff --git a/test/lib/code_corps/organizations/organizations_test.exs b/test/lib/code_corps/organizations/organizations_test.exs new file mode 100644 index 000000000..8ee0ece26 --- /dev/null +++ b/test/lib/code_corps/organizations/organizations_test.exs @@ -0,0 +1,45 @@ +defmodule CodeCorps.OrganizationsTest do + use CodeCorps.DbAccessCase + + alias CodeCorps.{Organization, Organizations, OrganizationInvite} + alias Ecto.Changeset + + describe "create/1" do + test "creates an organization" do + %{id: owner_id} = insert(:user) + attrs = %{ + "cloudinary_public_id" => "Baz", + "description" => "Bar", + "name" => "Foo", + "owner_id" => owner_id + } + {:ok, %Organization{} = organization} = Organizations.create(attrs) + + assert organization.name == "Foo" + assert organization.description == "Bar" + assert organization.cloudinary_public_id == "Baz" + end + + test "returns changeset tuple if there are validation errors"do + {:error, %Changeset{} = changeset} = Organizations.create(%{}) + refute changeset.valid? + end + + test "fulfills associated organization invite if invite code provided" do + %{code: invite_code, id: invite_id} = insert(:organization_invite) + %{id: owner_id} = insert(:user) + attrs = %{ + "cloudinary_public_id" => "Baz", + "description" => "Bar", + "invite_code" => invite_code, + "name" => "Foo", + "owner_id" => owner_id + } + {:ok, %Organization{id: organization_id}} = Organizations.create(attrs) + + associated_organization_id = + OrganizationInvite |> Repo.get(invite_id) |> Map.get(:organization_id) + assert associated_organization_id == organization_id + end + end +end diff --git a/test/lib/code_corps/policy/organization_test.exs b/test/lib/code_corps/policy/organization_test.exs index df920d023..3b472a869 100644 --- a/test/lib/code_corps/policy/organization_test.exs +++ b/test/lib/code_corps/policy/organization_test.exs @@ -9,24 +9,25 @@ defmodule CodeCorps.Policy.OrganizationTest do assert create?(user, %{}) end - test "returns true when there is correct code" do + test "returns true when the code is correct" do user = build(:user, admin: false) organization_invite = insert(:organization_invite) - params = %{"code" => organization_invite.code} + params = %{"invite_code" => organization_invite.code} assert create?(user, params) end test "returns false when code is incorrect" do user = build(:user, admin: false) insert(:organization_invite) - params = %{"code" => "incorrect"} + params = %{"invite_code" => "incorrect"} refute create?(user, params) end - test "returns false when code is correct but OrganizationInvite is fulfilled" do + test "returns false when code is correct but is associated with an organization" do user = build(:user, admin: false) - organization_invite = insert(:organization_invite, fulfilled: true) - params = %{"code" => organization_invite.code} + organization = insert(:organization); + organization_invite = build(:organization_invite, organization: organization) + params = %{"invite_code" => organization_invite.code} refute create?(user, params) end end diff --git a/test/lib/code_corps_web/controllers/organization_controller_test.exs b/test/lib/code_corps_web/controllers/organization_controller_test.exs index 1522013d0..0b9f84cc3 100644 --- a/test/lib/code_corps_web/controllers/organization_controller_test.exs +++ b/test/lib/code_corps_web/controllers/organization_controller_test.exs @@ -1,7 +1,11 @@ defmodule CodeCorpsWeb.OrganizationControllerTest do use CodeCorpsWeb.ApiCase, resource_name: :organization - @valid_attrs %{description: "Build a better future.", name: "Code Corps"} + @valid_attrs %{ + cloudinary_public_id: "foo", + description: "Build a better future.", + name: "Code Corps" + } @invalid_attrs %{name: ""} describe "index" do @@ -42,23 +46,36 @@ defmodule CodeCorpsWeb.OrganizationControllerTest do end describe "create" do - @tag authenticated: :admin - test "creates and renders resource when data is valid", %{conn: conn, current_user: user} do - attrs = Map.merge(@valid_attrs, %{owner: user}) + @tag :authenticated + test "creates and renders resource when data is valid and invite exists", %{conn: conn, current_user: user} do + insert(:organization_invite, code: "valid") + attrs = Map.merge(@valid_attrs, %{owner: user, invite_code: "valid"}) assert conn |> request_create(attrs) |> json_response(201) end + @tag :authenticated + test "renders 403 when data is valid but invite does not exist", %{conn: conn, current_user: user} do + attrs = Map.merge(@valid_attrs, %{owner: user, invite_code: "invalid"}) + assert conn |> request_create(attrs) |> json_response(403) + end + @tag authenticated: :admin test "renders 422 when data is invalid", %{conn: conn} do assert conn |> request_create(@invalid_attrs) |> json_response(422) end + @tag authenticated: :admin + test "creates and renders resource when data is valid and user is admin", %{conn: conn, current_user: user} do + attrs = Map.merge(@valid_attrs, %{owner: user}) + assert conn |> request_create(attrs) |> json_response(201) + end + test "renders 401 when not authenticated", %{conn: conn} do assert conn |> request_create |> json_response(401) end @tag :authenticated - test "renders 403 when not authorized", %{conn: conn} do + test "renders 403 when data is valid, but no invite and user not admin", %{conn: conn} do assert conn |> request_create |> json_response(403) end end @@ -86,6 +103,6 @@ defmodule CodeCorpsWeb.OrganizationControllerTest do @tag :authenticated test "renders 404 when id is nonexistent", %{conn: conn} do assert conn |> request_update(:not_found) |> json_response(404) - end + end end end diff --git a/test/lib/code_corps_web/views/changeset_view_test.exs b/test/lib/code_corps_web/views/changeset_view_test.exs index a10ad1aa0..4d5a92f84 100644 --- a/test/lib/code_corps_web/views/changeset_view_test.exs +++ b/test/lib/code_corps_web/views/changeset_view_test.exs @@ -21,7 +21,7 @@ defmodule CodeCorpsWeb.ChangesetViewTest do %{ detail: "User can't be blank", source: %{ - pointer: "data/attributes/user_id" + pointer: "data/attributes/user-id" }, status: "422", title: "can't be blank" diff --git a/test/lib/code_corps_web/views/organization_invite_view_test.exs b/test/lib/code_corps_web/views/organization_invite_view_test.exs index 1048a1366..80d7e863c 100644 --- a/test/lib/code_corps_web/views/organization_invite_view_test.exs +++ b/test/lib/code_corps_web/views/organization_invite_view_test.exs @@ -4,7 +4,8 @@ defmodule CodeCorpsWeb.OrganizationInviteViewTest do use CodeCorpsWeb.ViewCase test "renders all attributes and relationships properly" do - organization_invite = insert(:organization_invite) + organization = insert(:organization) + organization_invite = insert(:organization_invite, organization: organization) rendered_json = render(CodeCorpsWeb.OrganizationInviteView, "show.json-api", data: organization_invite) @@ -14,10 +15,17 @@ defmodule CodeCorpsWeb.OrganizationInviteViewTest do "type" => "organization-invite", "attributes" => %{ "email" => organization_invite.email, - "fulfilled" => organization_invite.fulfilled, "inserted-at" => organization_invite.inserted_at, "organization-name" => organization_invite.organization_name, "updated-at" => organization_invite.updated_at + }, + "relationships" => %{ + "organization" => %{ + "data" => %{ + "id" => organization.id |> Integer.to_string, + "type" => "organization" + } + } } }, "jsonapi" => %{ diff --git a/test/support/factories.ex b/test/support/factories.ex index f697b0209..2a715e7a2 100644 --- a/test/support/factories.ex +++ b/test/support/factories.ex @@ -118,7 +118,6 @@ defmodule CodeCorps.Factories do %CodeCorps.OrganizationInvite{ code: sequence(:code, &"n43crhiqR-#{&1}"), email: sequence(:email, &"email_#{&1}@mail.com"), - fulfilled: false, organization_name: sequence(:organization_name, &"organization-#{&1}") } end