diff --git a/.formatter.exs b/.formatter.exs index 7e4d805..51da9f5 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,6 +1,11 @@ [ import_deps: [:ecto, :ecto_sql, :phoenix], subdirectories: ["priv/*/migrations"], - plugins: [Phoenix.LiveView.HTMLFormatter], - inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}", "priv/*/seeds.exs"] + plugins: [Phoenix.LiveView.HTMLFormatter, Absinthe.Formatter], + inputs: [ + "*.{ex,exs}", + "{config,lib,test}/**/*.{ex,exs}", + "priv/*/seeds.exs", + "{lib,priv}/**/*.{gql,graphql}" + ] ] diff --git a/lib/swapi/dataloader.ex b/lib/swapi/dataloader.ex new file mode 100644 index 0000000..25884c9 --- /dev/null +++ b/lib/swapi/dataloader.ex @@ -0,0 +1,9 @@ +defmodule SWAPI.Dataloader do + @moduledoc """ + Dataloader for GraphQL + """ + + def data, do: Dataloader.Ecto.new(SWAPI.Repo, query: &query/2) + + def query(queryable, _params), do: queryable +end diff --git a/lib/swapi_web/graphql/queries.ex b/lib/swapi_web/graphql/queries.ex new file mode 100644 index 0000000..b577dcb --- /dev/null +++ b/lib/swapi_web/graphql/queries.ex @@ -0,0 +1,23 @@ +defmodule SWAPIWeb.GraphQL.Queries do + @moduledoc """ + GraphQL queries + """ + + use Absinthe.Schema.Notation + + import_types(SWAPIWeb.GraphQL.Queries.FilmQueries) + import_types(SWAPIWeb.GraphQL.Queries.PersonQueries) + import_types(SWAPIWeb.GraphQL.Queries.PlanetQueries) + import_types(SWAPIWeb.GraphQL.Queries.SpeciesQueries) + import_types(SWAPIWeb.GraphQL.Queries.StarshipQueries) + import_types(SWAPIWeb.GraphQL.Queries.VehicleQueries) + + object :queries do + import_fields(:film_queries) + import_fields(:person_queries) + import_fields(:planet_queries) + import_fields(:species_queries) + import_fields(:starship_queries) + import_fields(:vehicle_queries) + end +end diff --git a/lib/swapi_web/graphql/queries/film_queries.ex b/lib/swapi_web/graphql/queries/film_queries.ex new file mode 100644 index 0000000..99de57d --- /dev/null +++ b/lib/swapi_web/graphql/queries/film_queries.ex @@ -0,0 +1,32 @@ +defmodule SWAPIWeb.GraphQL.Queries.FilmQueries do + @moduledoc """ + GraphQL queries for films + """ + + use Absinthe.Schema.Notation + + alias SWAPIWeb.GraphQL.Resolvers.FilmResolver + + object :film_queries do + @desc "Get all films." + field :all_films, list_of(:film) do + resolve(&FilmResolver.all/2) + end + + @desc "Get a film by ID." + field :film, :film do + @desc "The ID of the film." + arg(:id, non_null(:id)) + + resolve(&FilmResolver.one/2) + end + + @desc "Search films by title." + field :search_films, list_of(:film) do + @desc "A list of search terms. If multiple search terms are used then objects will be returned in the list only if all the provided terms are matched." + arg(:search_terms, non_null(list_of(non_null(:string)))) + + resolve(&FilmResolver.search/2) + end + end +end diff --git a/lib/swapi_web/graphql/queries/person_queries.ex b/lib/swapi_web/graphql/queries/person_queries.ex new file mode 100644 index 0000000..98e7101 --- /dev/null +++ b/lib/swapi_web/graphql/queries/person_queries.ex @@ -0,0 +1,32 @@ +defmodule SWAPIWeb.GraphQL.Queries.PersonQueries do + @moduledoc """ + GraphQL queries for people + """ + + use Absinthe.Schema.Notation + + alias SWAPIWeb.GraphQL.Resolvers.PersonResolver + + object :person_queries do + @desc "Get all people." + field :all_people, list_of(:person) do + resolve(&PersonResolver.all/2) + end + + @desc "Get a person by ID." + field :person, :person do + @desc "The ID of the person." + arg(:id, non_null(:id)) + + resolve(&PersonResolver.one/2) + end + + @desc "Search people by name." + field :search_people, list_of(:person) do + @desc "A list of search terms. If multiple search terms are used then objects will be returned in the list only if all the provided terms are matched." + arg(:search_terms, non_null(list_of(non_null(:string)))) + + resolve(&PersonResolver.search/2) + end + end +end diff --git a/lib/swapi_web/graphql/queries/planet_queries.ex b/lib/swapi_web/graphql/queries/planet_queries.ex new file mode 100644 index 0000000..9fec459 --- /dev/null +++ b/lib/swapi_web/graphql/queries/planet_queries.ex @@ -0,0 +1,32 @@ +defmodule SWAPIWeb.GraphQL.Queries.PlanetQueries do + @moduledoc """ + GraphQL queries for planets + """ + + use Absinthe.Schema.Notation + + alias SWAPIWeb.GraphQL.Resolvers.PlanetResolver + + object :planet_queries do + @desc "Get all planets." + field :all_planets, list_of(:planet) do + resolve(&PlanetResolver.all/2) + end + + @desc "Get a planet by ID." + field :planet, :planet do + @desc "The ID of the planet." + arg(:id, non_null(:id)) + + resolve(&PlanetResolver.one/2) + end + + @desc "Search planets by name." + field :search_planets, list_of(:planet) do + @desc "A list of search terms. If multiple search terms are used then objects will be returned in the list only if all the provided terms are matched." + arg(:search_terms, non_null(list_of(non_null(:string)))) + + resolve(&PlanetResolver.search/2) + end + end +end diff --git a/lib/swapi_web/graphql/queries/species_queries.ex b/lib/swapi_web/graphql/queries/species_queries.ex new file mode 100644 index 0000000..7cdf06f --- /dev/null +++ b/lib/swapi_web/graphql/queries/species_queries.ex @@ -0,0 +1,32 @@ +defmodule SWAPIWeb.GraphQL.Queries.SpeciesQueries do + @moduledoc """ + GraphQL queries for species + """ + + use Absinthe.Schema.Notation + + alias SWAPIWeb.GraphQL.Resolvers.SpeciesResolver + + object :species_queries do + @desc "Get all species." + field :all_species, list_of(:species) do + resolve(&SpeciesResolver.all/2) + end + + @desc "Get a species by ID." + field :species, :species do + @desc "The ID of the species." + arg(:id, non_null(:id)) + + resolve(&SpeciesResolver.one/2) + end + + @desc "Search species by name." + field :search_species, list_of(:species) do + @desc "A list of search terms. If multiple search terms are used then objects will be returned in the list only if all the provided terms are matched." + arg(:search_terms, non_null(list_of(non_null(:string)))) + + resolve(&SpeciesResolver.search/2) + end + end +end diff --git a/lib/swapi_web/graphql/queries/starship_queries.ex b/lib/swapi_web/graphql/queries/starship_queries.ex new file mode 100644 index 0000000..860f4be --- /dev/null +++ b/lib/swapi_web/graphql/queries/starship_queries.ex @@ -0,0 +1,32 @@ +defmodule SWAPIWeb.GraphQL.Queries.StarshipQueries do + @moduledoc """ + GraphQL queries for starships + """ + + use Absinthe.Schema.Notation + + alias SWAPIWeb.GraphQL.Resolvers.StarshipResolver + + object :starship_queries do + @desc "Get all starships." + field :all_starships, list_of(:starship) do + resolve(&StarshipResolver.all/2) + end + + @desc "Get a starship by ID." + field :starship, :starship do + @desc "The ID of the starship." + arg(:id, non_null(:id)) + + resolve(&StarshipResolver.one/2) + end + + @desc "Search starships by name or model." + field :search_starships, list_of(:starship) do + @desc "A list of search terms. If multiple search terms are used then objects will be returned in the list only if all the provided terms are matched." + arg(:search_terms, non_null(list_of(non_null(:string)))) + + resolve(&StarshipResolver.search/2) + end + end +end diff --git a/lib/swapi_web/graphql/queries/vehicle_queries.ex b/lib/swapi_web/graphql/queries/vehicle_queries.ex new file mode 100644 index 0000000..2b472d0 --- /dev/null +++ b/lib/swapi_web/graphql/queries/vehicle_queries.ex @@ -0,0 +1,32 @@ +defmodule SWAPIWeb.GraphQL.Queries.VehicleQueries do + @moduledoc """ + GraphQL queries for vehicles + """ + + use Absinthe.Schema.Notation + + alias SWAPIWeb.GraphQL.Resolvers.VehicleResolver + + object :vehicle_queries do + @desc "Get all vehicles." + field :all_vehicles, list_of(:vehicle) do + resolve(&VehicleResolver.all/2) + end + + @desc "Get a vehicle by ID." + field :vehicle, :vehicle do + @desc "The ID of the vehicle." + arg(:id, non_null(:id)) + + resolve(&VehicleResolver.one/2) + end + + @desc "Search vehicles by name or model." + field :search_vehicles, list_of(:vehicle) do + @desc "A list of search terms. If multiple search terms are used then objects will be returned in the list only if all the provided terms are matched." + arg(:search_terms, non_null(list_of(non_null(:string)))) + + resolve(&VehicleResolver.search/2) + end + end +end diff --git a/lib/swapi_web/graphql/resolvers/film_resolver.ex b/lib/swapi_web/graphql/resolvers/film_resolver.ex new file mode 100644 index 0000000..bd8bf94 --- /dev/null +++ b/lib/swapi_web/graphql/resolvers/film_resolver.ex @@ -0,0 +1,26 @@ +defmodule SWAPIWeb.GraphQL.Resolvers.FilmResolver do + @moduledoc """ + Film resolver. + """ + + alias SWAPI.Films + alias SWAPI.Schemas.Film + + @spec all(map, map) :: {:ok, list(Film.t())} | {:error, any} + def all(_args, _info) do + {:ok, Films.list_films()} + end + + @spec one(map, Absinthe.Blueprint.t()) :: {:ok, Film.t()} | {:error, any} + def one(%{id: id}, _info) do + case Films.get_film(id) do + {:ok, film} -> {:ok, film} + {:error, :not_found} -> {:error, "Film not found"} + end + end + + @spec search(map, Absinthe.Blueprint.t()) :: {:ok, list(Film.t())} | {:error, any} + def search(%{search_terms: search_terms}, _info) do + {:ok, Films.search_films(search_terms)} + end +end diff --git a/lib/swapi_web/graphql/resolvers/person_resolver.ex b/lib/swapi_web/graphql/resolvers/person_resolver.ex new file mode 100644 index 0000000..1dd34f9 --- /dev/null +++ b/lib/swapi_web/graphql/resolvers/person_resolver.ex @@ -0,0 +1,26 @@ +defmodule SWAPIWeb.GraphQL.Resolvers.PersonResolver do + @moduledoc """ + Person resolver. + """ + + alias SWAPI.People + alias SWAPI.Schemas.Person + + @spec all(map, map) :: {:ok, list(Person.t())} | {:error, any} + def all(_args, _info) do + {:ok, People.list_people()} + end + + @spec one(map, Absinthe.Resolution.t()) :: {:ok, Person.t()} | {:error, any} + def one(%{id: id}, _info) do + case People.get_person(id) do + {:ok, person} -> {:ok, person} + {:error, :not_found} -> {:error, "Person not found"} + end + end + + @spec search(map, Absinthe.Blueprint.t()) :: {:ok, list(Person.t())} | {:error, any} + def search(%{search_terms: search_terms}, _info) do + {:ok, People.search_people(search_terms)} + end +end diff --git a/lib/swapi_web/graphql/resolvers/planet_resolver.ex b/lib/swapi_web/graphql/resolvers/planet_resolver.ex new file mode 100644 index 0000000..72b463c --- /dev/null +++ b/lib/swapi_web/graphql/resolvers/planet_resolver.ex @@ -0,0 +1,26 @@ +defmodule SWAPIWeb.GraphQL.Resolvers.PlanetResolver do + @moduledoc """ + Planet resolver. + """ + + alias SWAPI.Planets + alias SWAPI.Schemas.Planet + + @spec all(map, map) :: {:ok, list(Planet.t())} | {:error, any} + def all(_args, _info) do + {:ok, Planets.list_planets()} + end + + @spec one(map, Absinthe.Resolution.t()) :: {:ok, Planet.t()} | {:error, any} + def one(%{id: id}, _info) do + case Planets.get_planet(id) do + {:ok, planet} -> {:ok, planet} + {:error, :not_found} -> {:error, "Planet not found"} + end + end + + @spec search(map, Absinthe.Blueprint.t()) :: {:ok, list(Planet.t())} | {:error, any} + def search(%{search_terms: search_terms}, _info) do + {:ok, Planets.search_planets(search_terms)} + end +end diff --git a/lib/swapi_web/graphql/resolvers/species_resolver.ex b/lib/swapi_web/graphql/resolvers/species_resolver.ex new file mode 100644 index 0000000..79c7def --- /dev/null +++ b/lib/swapi_web/graphql/resolvers/species_resolver.ex @@ -0,0 +1,26 @@ +defmodule SWAPIWeb.GraphQL.Resolvers.SpeciesResolver do + @moduledoc """ + Species resolver. + """ + + alias SWAPI.Schemas.Species, as: SpeciesSchema + alias SWAPI.Species + + @spec all(map, map) :: {:ok, list(SpeciesSchema.t())} | {:error, any} + def all(_args, _info) do + {:ok, Species.list_species()} + end + + @spec one(map, Absinthe.Resolution.t()) :: {:ok, SpeciesSchema.t()} | {:error, any} + def one(%{id: id}, _info) do + case Species.get_species(id) do + {:ok, species} -> {:ok, species} + {:error, :not_found} -> {:error, "Species not found"} + end + end + + @spec search(map, Absinthe.Blueprint.t()) :: {:ok, list(SpeciesSchema.t())} | {:error, any} + def search(%{search_terms: search_terms}, _info) do + {:ok, Species.search_species(search_terms)} + end +end diff --git a/lib/swapi_web/graphql/resolvers/starship_resolver.ex b/lib/swapi_web/graphql/resolvers/starship_resolver.ex new file mode 100644 index 0000000..f44c33d --- /dev/null +++ b/lib/swapi_web/graphql/resolvers/starship_resolver.ex @@ -0,0 +1,26 @@ +defmodule SWAPIWeb.GraphQL.Resolvers.StarshipResolver do + @moduledoc """ + Starship resolver. + """ + + alias SWAPI.Schemas.Starship + alias SWAPI.Starships + + @spec all(map, map) :: {:ok, list(Starship.t())} | {:error, any} + def all(_args, _info) do + {:ok, Starships.list_starships()} + end + + @spec one(map, Absinthe.Resolution.t()) :: {:ok, Starship.t()} | {:error, any} + def one(%{id: id}, _info) do + case Starships.get_starship(id) do + {:ok, starship} -> {:ok, starship} + {:error, :not_found} -> {:error, "Starship not found"} + end + end + + @spec search(map, Absinthe.Blueprint.t()) :: {:ok, list(Starship.t())} | {:error, any} + def search(%{search_terms: search_terms}, _info) do + {:ok, Starships.search_starships(search_terms)} + end +end diff --git a/lib/swapi_web/graphql/resolvers/vehicle_resolver.ex b/lib/swapi_web/graphql/resolvers/vehicle_resolver.ex new file mode 100644 index 0000000..a5d8fb1 --- /dev/null +++ b/lib/swapi_web/graphql/resolvers/vehicle_resolver.ex @@ -0,0 +1,26 @@ +defmodule SWAPIWeb.GraphQL.Resolvers.VehicleResolver do + @moduledoc """ + Vehicle resolver. + """ + + alias SWAPI.Schemas.Vehicle + alias SWAPI.Vehicles + + @spec all(map, map) :: {:ok, list(Vehicle.t())} | {:error, any} + def all(_args, _info) do + {:ok, Vehicles.list_vehicles()} + end + + @spec one(map, Absinthe.Resolution.t()) :: {:ok, Vehicle.t()} | {:error, any} + def one(%{id: id}, _info) do + case Vehicles.get_vehicle(id) do + {:ok, vehicle} -> {:ok, vehicle} + {:error, :not_found} -> {:error, "Vehicle not found"} + end + end + + @spec search(map, Absinthe.Blueprint.t()) :: {:ok, list(Vehicle.t())} | {:error, any} + def search(%{search_terms: search_terms}, _info) do + {:ok, Vehicles.search_vehicles(search_terms)} + end +end diff --git a/lib/swapi_web/graphql/schema.ex b/lib/swapi_web/graphql/schema.ex new file mode 100644 index 0000000..8caa2e3 --- /dev/null +++ b/lib/swapi_web/graphql/schema.ex @@ -0,0 +1,26 @@ +defmodule SWAPIWeb.GraphQL.Schema do + @moduledoc """ + GraphQL schema + """ + + use Absinthe.Schema + + import_types(SWAPIWeb.GraphQL.Types) + import_types(SWAPIWeb.GraphQL.Queries) + + query do + import_fields(:queries) + end + + def context(ctx) do + loader = + Dataloader.new() + |> Dataloader.add_source(SWAPI.Dataloader, SWAPI.Dataloader.data()) + + Map.put(ctx, :loader, loader) + end + + def plugins do + [Absinthe.Middleware.Dataloader] ++ Absinthe.Plugin.defaults() + end +end diff --git a/lib/swapi_web/graphql/types.ex b/lib/swapi_web/graphql/types.ex new file mode 100644 index 0000000..3dc0693 --- /dev/null +++ b/lib/swapi_web/graphql/types.ex @@ -0,0 +1,16 @@ +defmodule SWAPIWeb.GraphQL.Types do + @moduledoc """ + GrahQL types + """ + + use Absinthe.Schema.Notation + + import_types(Absinthe.Type.Custom) + + import_types(SWAPIWeb.GraphQL.Types.Film) + import_types(SWAPIWeb.GraphQL.Types.Person) + import_types(SWAPIWeb.GraphQL.Types.Planet) + import_types(SWAPIWeb.GraphQL.Types.Species) + import_types(SWAPIWeb.GraphQL.Types.Starship) + import_types(SWAPIWeb.GraphQL.Types.Vehicle) +end diff --git a/lib/swapi_web/graphql/types/film.ex b/lib/swapi_web/graphql/types/film.ex new file mode 100644 index 0000000..226fba6 --- /dev/null +++ b/lib/swapi_web/graphql/types/film.ex @@ -0,0 +1,64 @@ +defmodule SWAPIWeb.GraphQL.Types.Film do + @moduledoc """ + GraphQL schema for films + """ + + use Absinthe.Schema.Notation + + import Absinthe.Resolution.Helpers + + @desc "The Film type represents a single film." + object :film do + @desc "A unique ID for this film." + field :id, :id + + @desc "The title of this film." + field :title, :string + + @desc "The episode number of this film." + field :episode_id, :integer + + @desc "The opening paragraphs at the beginning of this film." + field :opening_crawl, :string + + @desc "The name of the director of this film." + field :director, :string + + @desc "The name(s) of the producer(s) of this film. Comma separated." + field :producer, :string + + @desc "The date of film release at original creator country." + field :release_date, :date + + @desc "A list of species that are in this film." + field :species, list_of(:species) do + resolve(dataloader(SWAPI.Dataloader)) + end + + @desc "A list of starships that are in this film." + field :starships, list_of(:starship) do + resolve(dataloader(SWAPI.Dataloader)) + end + + @desc "A list of vehicles that are in this film." + field :vehicles, list_of(:vehicle) do + resolve(dataloader(SWAPI.Dataloader)) + end + + @desc "A list of people that are in this film." + field :characters, list_of(:person) do + resolve(dataloader(SWAPI.Dataloader)) + end + + @desc "A list of planets that are in this film." + field :planets, list_of(:planet) do + resolve(dataloader(SWAPI.Dataloader)) + end + + @desc "The time that this resource was created." + field :created, :datetime + + @desc "The time that this resource was edited." + field :edited, :datetime + end +end diff --git a/lib/swapi_web/graphql/types/person.ex b/lib/swapi_web/graphql/types/person.ex new file mode 100644 index 0000000..3a0d6a4 --- /dev/null +++ b/lib/swapi_web/graphql/types/person.ex @@ -0,0 +1,70 @@ +defmodule SWAPIWeb.GraphQL.Types.Person do + @moduledoc """ + GraphQL schema for people + """ + + use Absinthe.Schema.Notation + + import Absinthe.Resolution.Helpers + + @desc "The Person type represents an individual person or character within the Star Wars universe." + object :person do + @desc "A unique ID for this person." + field :id, :id + + @desc "The name of this person." + field :name, :string + + @desc "The birth year of the person, using the in-universe standard of **BBY** or **ABY** - Before the Battle of Yavin or After the Battle of Yavin. The Battle of Yavin is a battle that occurs at the end of Star Wars episode IV: A New Hope." + field :birth_year, :string + + @desc ~S(The eye color of this person. Will be "unknown" if not known or "n/a" if the person does not have an eye.) + field :eye_color, :string + + @desc ~S(The gender of this person. Either "Male", "Female" or "unknown", "n/a" if the person does not have a gender.) + field :gender, :string + + @desc ~S(The hair color of this person. Will be "unknown" if not known or "n/a" if the person does not have hair.) + field :hair_color, :string + + @desc "The height of the person in centimeters." + field :height, :string + + @desc "The mass of the person in kilograms." + field :mass, :string + + @desc "The skin color of this person." + field :skin_color, :string + + @desc "The planet that this person was born on or inhabits." + field :homeworld, :planet do + resolve(dataloader(SWAPI.Dataloader)) + end + + @desc "A list of films that this person has been in." + field :films, list_of(:film) do + resolve(dataloader(SWAPI.Dataloader)) + end + + @desc "A list of species that this person belongs to." + field :species, list_of(:species) do + resolve(dataloader(SWAPI.Dataloader)) + end + + @desc "A list of starships that this person has piloted." + field :starships, list_of(:starship) do + resolve(dataloader(SWAPI.Dataloader)) + end + + @desc "A list of vehicles that this person has piloted." + field :vehicles, list_of(:vehicle) do + resolve(dataloader(SWAPI.Dataloader)) + end + + @desc "The time that this resource was created." + field :created, :datetime + + @desc "The time that this resource was edited." + field :edited, :datetime + end +end diff --git a/lib/swapi_web/graphql/types/planet.ex b/lib/swapi_web/graphql/types/planet.ex new file mode 100644 index 0000000..9b9fe8d --- /dev/null +++ b/lib/swapi_web/graphql/types/planet.ex @@ -0,0 +1,63 @@ +defmodule SWAPIWeb.GraphQL.Types.Planet do + @moduledoc """ + GraphQL schema for planets + """ + + use Absinthe.Schema.Notation + + import Absinthe.Resolution.Helpers + + @desc "The Planet type represents a large mass, planet or planetoid in the Star Wars Universe, at the time of 0 ABY." + object :planet do + @desc "A unique ID for this planet." + field :id, :id + + @desc "The name of this planet." + field :name, :string + + @desc "The diameter of this planet in kilometers." + field :diameter, :string + + @desc "The number of standard hours it takes for this planet to complete a single rotation on its axis." + field :rotation_period, :string + + @desc "The number of standard days it takes for this planet to complete a single orbit of its local star." + field :orbital_period, :string + + @desc ~S(A number denoting the gravity of this planet, where "1" is normal or 1 standard G. "2" is twice or 2 standard Gs. "0.5" is half or 0.5 standard Gs.) + field :gravity, :string + + @desc "The average population of sentient beings inhabiting this planet." + field :population, :string + + @desc "The climate of this planet. Comma separated if diverse." + field :climate, :string + + @desc "The terrain of this planet. Comma separated if diverse." + field :terrain, :string + + @desc "The percentage of the planet surface that is naturally occurring water or bodies of water." + field :surface_water, :string + + @desc "A list of people that live on this planet." + field :residents, list_of(:person) do + resolve(dataloader(SWAPI.Dataloader)) + end + + @desc "A list of species that live on this planet." + field :species, list_of(:species) do + resolve(dataloader(SWAPI.Dataloader)) + end + + @desc "A list of films that this planet has appeared in." + field :films, list_of(:film) do + resolve(dataloader(SWAPI.Dataloader)) + end + + @desc "The time that this resource was created." + field :created, :datetime + + @desc "The time that this resource was edited." + field :edited, :datetime + end +end diff --git a/lib/swapi_web/graphql/types/species.ex b/lib/swapi_web/graphql/types/species.ex new file mode 100644 index 0000000..68fed31 --- /dev/null +++ b/lib/swapi_web/graphql/types/species.ex @@ -0,0 +1,63 @@ +defmodule SWAPIWeb.GraphQL.Types.Species do + @moduledoc """ + GraphQL schema for species + """ + + use Absinthe.Schema.Notation + + import Absinthe.Resolution.Helpers + + @desc "The Species type represents a type of person or character within the Star Wars Universe." + object :species do + @desc "A unique ID for this species." + field :id, :id + + @desc "The name of this species." + field :name, :string + + @desc ~S(The classification of this species, such as "mammal" or "reptile".) + field :classification, :string + + @desc "The designation of this species, such as \"sentient\"." + field :designation, :string + + @desc "The average height of this species in centimeters." + field :average_height, :string + + @desc "The average lifespan of this species in years." + field :average_lifespan, :string + + @desc "A comma-separated string of common eye colors for this species, \"none\" if this species does not typically have eyes." + field :eye_colors, :string + + @desc "A comma-separated string of common hair colors for this species, \"none\" if this species does not typically have hair." + field :hair_colors, :string + + @desc "A comma-separated string of common skin colors for this species, \"none\" if this species does not typically have skin." + field :skin_colors, :string + + @desc "The language commonly spoken by this species." + field :language, :string + + @desc "The planet that this species originates from." + field :homeworld, :planet do + resolve(dataloader(SWAPI.Dataloader)) + end + + @desc "A list of people that are a part of this species." + field :people, list_of(:person) do + resolve(dataloader(SWAPI.Dataloader)) + end + + @desc "A list of films that this species has appeared in." + field :films, list_of(:film) do + resolve(dataloader(SWAPI.Dataloader)) + end + + @desc "The time that this resource was created." + field :created, :datetime + + @desc "The time that this resource was edited." + field :edited, :datetime + end +end diff --git a/lib/swapi_web/graphql/types/starship.ex b/lib/swapi_web/graphql/types/starship.ex new file mode 100644 index 0000000..4ddb63d --- /dev/null +++ b/lib/swapi_web/graphql/types/starship.ex @@ -0,0 +1,95 @@ +defmodule SWAPIWeb.GraphQL.Types.Starship do + @moduledoc """ + GraphQL schema for starships + """ + + use Absinthe.Schema.Notation + + import Absinthe.Resolution.Helpers + import SWAPIWeb.GraphQL.Util + + @desc "The Starship type represents a single transport craft that has hyperdrive capability." + object :starship do + @desc "A unique ID for this starship." + field :id, :id + + @desc "The name of this starship. The common name, such as \"Death Star\"." + field :name, :string do + resolve(dataloader(SWAPI.Dataloader, :transport, callback: &transport_field_callback/4)) + end + + @desc ~S(The model or official name of this starship. Such as "T-65 X-wing" or "DS-1 Orbital Battle Station".) + field :model, :string do + resolve(dataloader(SWAPI.Dataloader, :transport, callback: &transport_field_callback/4)) + end + + @desc ~S(The class of this starship, such as "Starfighter" or "Deep Space Mobile Battlestation") + field :starship_class, :string + + @desc "The manufacturer of this starship. Comma separated if more than one." + field :manufacturer, :string do + resolve(dataloader(SWAPI.Dataloader, :transport, callback: &transport_field_callback/4)) + end + + @desc "The cost of this starship new, in galactic credits." + field :cost_in_credits, :string do + resolve(dataloader(SWAPI.Dataloader, :transport, callback: &transport_field_callback/4)) + end + + @desc "The length of this starship in meters." + field :length, :string do + resolve(dataloader(SWAPI.Dataloader, :transport, callback: &transport_field_callback/4)) + end + + @desc "The number of personnel needed to run or pilot this starship." + field :crew, :string do + resolve(dataloader(SWAPI.Dataloader, :transport, callback: &transport_field_callback/4)) + end + + @desc "The number of non-essential people this starship can transport." + field :passengers, :string do + resolve(dataloader(SWAPI.Dataloader, :transport, callback: &transport_field_callback/4)) + end + + @desc "The maximum speed of this starship in the atmosphere. \"N/A\" if this starship is incapable of atmospheric flight." + field :max_atmosphering_speed, :string do + resolve(dataloader(SWAPI.Dataloader, :transport, callback: &transport_field_callback/4)) + end + + @desc "The class of this starships hyperdrive." + field :hyperdrive_rating, :string + + @desc "The Maximum number of Megalights this starship can travel in a standard hour. A \"Megalight\" is a standard unit of distance and has never been defined before within the Star Wars universe. This figure is only really useful for measuring the difference in speed of starships. We can assume it is similar to AU, the distance between our Sun (Sol) and Earth." + field :mglt, :string + + @desc "The maximum number of kilograms that this starship can transport." + field :cargo_capacity, :string do + resolve(dataloader(SWAPI.Dataloader, :transport, callback: &transport_field_callback/4)) + end + + @desc "The maximum length of time that this starship can provide consumables for its entire crew without having to resupply." + field :consumables, :string do + resolve(dataloader(SWAPI.Dataloader, :transport, callback: &transport_field_callback/4)) + end + + @desc "A list of films that this starship has appeared in." + field :films, list_of(:film) do + resolve(dataloader(SWAPI.Dataloader)) + end + + @desc "A list of people that this starship has been piloted by." + field :pilots, list_of(:person) do + resolve(dataloader(SWAPI.Dataloader)) + end + + @desc "The time that this resource was created." + field :created, :datetime do + resolve(dataloader(SWAPI.Dataloader, :transport, callback: &transport_field_callback/4)) + end + + @desc "The time that this resource was edited." + field :edited, :datetime do + resolve(dataloader(SWAPI.Dataloader, :transport, callback: &transport_field_callback/4)) + end + end +end diff --git a/lib/swapi_web/graphql/types/vehicle.ex b/lib/swapi_web/graphql/types/vehicle.ex new file mode 100644 index 0000000..eef0db0 --- /dev/null +++ b/lib/swapi_web/graphql/types/vehicle.ex @@ -0,0 +1,89 @@ +defmodule SWAPIWeb.GraphQL.Types.Vehicle do + @moduledoc """ + GraphQL schema for vehicles + """ + + use Absinthe.Schema.Notation + + import Absinthe.Resolution.Helpers + import SWAPIWeb.GraphQL.Util + + @desc "The Vehicle type represents a single transport craft that does not have hyperdrive capability." + object :vehicle do + @desc "A unique ID for this vehicle." + field :id, :id + + @desc ~S(The name of this vehicle. The common name, such as "Sand Crawler" or "Speeder bike".) + field :name, :string do + resolve(dataloader(SWAPI.Dataloader, :transport, callback: &transport_field_callback/4)) + end + + @desc "The model or official name of this vehicle. Such as \"All-Terrain Attack Transport\"." + field :model, :string do + resolve(dataloader(SWAPI.Dataloader, :transport, callback: &transport_field_callback/4)) + end + + @desc ~S(The class of this vehicle, such as "Wheeled" or "Repulsorcraft".) + field :vehicle_class, :string + + @desc "The manufacturer of this vehicle. Comma separated if more than one." + field :manufacturer, :string do + resolve(dataloader(SWAPI.Dataloader, :transport, callback: &transport_field_callback/4)) + end + + @desc "The length of this vehicle in meters." + field :cost_in_credits, :string do + resolve(dataloader(SWAPI.Dataloader, :transport, callback: &transport_field_callback/4)) + end + + @desc "The cost of this vehicle new, in Galactic Credits." + field :length, :string do + resolve(dataloader(SWAPI.Dataloader, :transport, callback: &transport_field_callback/4)) + end + + @desc "The number of personnel needed to run or pilot this vehicle." + field :crew, :string do + resolve(dataloader(SWAPI.Dataloader, :transport, callback: &transport_field_callback/4)) + end + + @desc "The number of non-essential people this vehicle can transport." + field :passengers, :string do + resolve(dataloader(SWAPI.Dataloader, :transport, callback: &transport_field_callback/4)) + end + + @desc "The maximum speed of this vehicle in the atmosphere." + field :max_atmosphering_speed, :string do + resolve(dataloader(SWAPI.Dataloader, :transport, callback: &transport_field_callback/4)) + end + + @desc "The maximum number of kilograms that this vehicle can transport." + field :cargo_capacity, :string do + resolve(dataloader(SWAPI.Dataloader, :transport, callback: &transport_field_callback/4)) + end + + @desc "The maximum length of time that this vehicle can provide consumables for its entire crew without having to resupply." + field :consumables, :string do + resolve(dataloader(SWAPI.Dataloader, :transport, callback: &transport_field_callback/4)) + end + + @desc "A list of films that this vehicle has appeared in." + field :films, list_of(:film) do + resolve(dataloader(SWAPI.Dataloader)) + end + + @desc "A list of people that this vehicle has been piloted by." + field :pilots, list_of(:person) do + resolve(dataloader(SWAPI.Dataloader)) + end + + @desc "The time that this resource was created." + field :created, :datetime do + resolve(dataloader(SWAPI.Dataloader, :transport, callback: &transport_field_callback/4)) + end + + @desc "The time that this resource was edited." + field :edited, :datetime do + resolve(dataloader(SWAPI.Dataloader, :transport, callback: &transport_field_callback/4)) + end + end +end diff --git a/lib/swapi_web/graphql/util.ex b/lib/swapi_web/graphql/util.ex new file mode 100644 index 0000000..9961cf6 --- /dev/null +++ b/lib/swapi_web/graphql/util.ex @@ -0,0 +1,15 @@ +defmodule SWAPIWeb.GraphQL.Util do + @moduledoc """ + Utility functions for the GraphQL API + """ + + def transport_field_callback(transport, _parent, _args, %{path: [field | _]}) do + case Map.get(transport, field.schema_node.identifier) do + nil -> {:error, "Invalid transport field"} + value -> {:ok, value} + end + end + + def transport_field_callback(_transport, _parent, _args, _info), + do: {:error, "Invalid transport field"} +end diff --git a/lib/swapi_web/router.ex b/lib/swapi_web/router.ex index 01c226f..14b9367 100644 --- a/lib/swapi_web/router.ex +++ b/lib/swapi_web/router.ex @@ -19,6 +19,10 @@ defmodule SWAPIWeb.Router do plug OpenApiSpex.Plug.PutApiSpec, module: SWAPIWeb.ApiSpec end + pipeline :graphql do + plug :accepts, ["json", "graphql-response+json"] + end + pipeline :browser do plug :accepts, ["html"] plug :fetch_session @@ -36,6 +40,11 @@ defmodule SWAPIWeb.Router do get "/", PageController, :home get "/postman", PageController, :postman get "/swaggerui", OpenApiSpex.Plug.SwaggerUI, path: "/api/openapi" + + get "/graphiql", Absinthe.Plug.GraphiQL, + schema: SWAPIWeb.GraphQL.Schema, + default_url: "/api/graphql", + interface: :playground end scope "/api" do @@ -53,6 +62,12 @@ defmodule SWAPIWeb.Router do get "/openapi", OpenApiSpex.Plug.RenderSpec, [] end + scope "/api/graphql" do + pipe_through :graphql + + forward "/", Absinthe.Plug, schema: SWAPIWeb.GraphQL.Schema + end + # Enable LiveDashboard and Swoosh mailbox preview in development if Application.compile_env(:swapi, :dev_routes) do # If you want to use the LiveDashboard in production, you should put diff --git a/mix.exs b/mix.exs index a5bce50..0afdfe4 100644 --- a/mix.exs +++ b/mix.exs @@ -32,6 +32,9 @@ defmodule SWAPI.MixProject do # Type `mix help deps` for examples and options. defp deps do [ + {:absinthe, "~> 1.7.0"}, + {:absinthe_plug, "~> 1.5"}, + {:dataloader, "~> 2.0.0"}, {:phoenix, "~> 1.7.9"}, {:phoenix_ecto, "~> 4.4"}, {:ecto_sql, "~> 3.10"}, diff --git a/mix.lock b/mix.lock index 85630fd..24f3edd 100644 --- a/mix.lock +++ b/mix.lock @@ -1,4 +1,6 @@ %{ + "absinthe": {:hex, :absinthe, "1.7.6", "0b897365f98d068cfcb4533c0200a8e58825a4aeeae6ec33633ebed6de11773b", [:mix], [{:dataloader, "~> 1.0.0 or ~> 2.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2.1", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7626951ca5eec627da960615b51009f3a774765406ff02722b1d818f17e5778"}, + "absinthe_plug": {:hex, :absinthe_plug, "1.5.8", "38d230641ba9dca8f72f1fed2dfc8abd53b3907d1996363da32434ab6ee5d6ab", [:mix], [{:absinthe, "~> 1.5", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bbb04176647b735828861e7b2705465e53e2cf54ccf5a73ddd1ebd855f996e5a"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "castore": {:hex, :castore, "1.0.5", "9eeebb394cc9a0f3ae56b813459f990abb0a3dedee1be6b27fdb50301930502f", [:mix], [], "hexpm", "8d7c597c3e4a64c395980882d4bca3cebb8d74197c590dc272cfd3b6a6310578"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.9", "e8d3364f310da6ce6463c3dd20cf90ae7bbecbf6c5203b98bf9b48035592649b", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "9dcab3d0f3038621f1601f13539e7a9ee99843862e66ad62827b0c42b2f58a54"}, @@ -8,6 +10,7 @@ "cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"}, "credo": {:hex, :credo, "1.7.3", "05bb11eaf2f2b8db370ecaa6a6bda2ec49b2acd5e0418bc106b73b07128c0436", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "35ea675a094c934c22fb1dca3696f3c31f2728ae6ef5a53b5d648c11180a4535"}, "dart_sass": {:hex, :dart_sass, "0.7.0", "7979e056cb74fd6843e1c72db763cffc7726a9192a657735b7d24c0d9c26a1ce", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "4a8e70bca41aa00846398abdf5ad8a64d7907a0f7bf40145cd2e40d5971629f2"}, + "dataloader": {:hex, :dataloader, "2.0.0", "49b42d60b9bb06d761a71d7b034c4b34787957e713d4fae15387a25fcd639112", [:mix], [{:ecto, ">= 3.4.3 and < 4.0.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:opentelemetry_process_propagator, "~> 0.2.1", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "09d61781b76ce216e395cdbc883ff00d00f46a503e215c22722dba82507dfef0"}, "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "dns_cluster": {:hex, :dns_cluster, "0.1.1", "73b4b2c3ec692f8a64276c43f8c929733a9ab9ac48c34e4c0b3d9d1b5cd69155", [:mix], [], "hexpm", "03a3f6ff16dcbb53e219b99c7af6aab29eb6b88acf80164b4bd76ac18dc890b3"}, diff --git a/test/swapi_web/graphql/queries/film_queries_test.exs b/test/swapi_web/graphql/queries/film_queries_test.exs new file mode 100644 index 0000000..dc6c37a --- /dev/null +++ b/test/swapi_web/graphql/queries/film_queries_test.exs @@ -0,0 +1,182 @@ +defmodule SWAPIWeb.GraphQL.QueriesTests do + use SWAPIWeb.ConnCase + + import Ecto.Changeset + + import SWAPI.FilmsFixtures + import SWAPI.PeopleFixtures + import SWAPI.PlanetsFixtures + import SWAPI.SpeciesFixtures + import SWAPI.StarshipsFixtures + import SWAPI.VehiclesFixtures + + alias SWAPI.Repo + alias SWAPI.Schemas.Film + + setup do + film = + film_fixture() + |> Film.changeset(%{title: "A New Hope"}) + |> put_assoc(:species, [species_fixture()]) + |> put_assoc(:starships, [starship_fixture()]) + |> put_assoc(:vehicles, [vehicle_fixture()]) + |> put_assoc(:characters, [person_fixture()]) + |> put_assoc(:planets, [planet_fixture()]) + |> Repo.update!() + + {:ok, %{film: film}} + end + + describe "allFilms" do + test "returns all films", %{conn: conn, film: film1} do + film2 = film_fixture() + + query = """ + query { + allFilms { + id + } + } + """ + + conn = get(conn, "/api/graphql", query: query) + + assert %{ + "data" => %{ + "allFilms" => [ + %{"id" => film1_id}, + %{"id" => film2_id} + ] + } + } = json_response(conn, 200) + + assert ^film1_id = "#{film1.id}" + assert ^film2_id = "#{film2.id}" + end + end + + describe "searchFilms" do + test "returns matching films", %{conn: conn, film: film} do + film_fixture(%{title: "Empire Strikes Back"}) + + query = """ + query { + searchFilms(searchTerms: ["Hope"]) { + id + } + } + """ + + conn = get(conn, "/api/graphql", query: query) + + assert %{ + "data" => %{ + "searchFilms" => [ + %{"id" => film_id} + ] + } + } = json_response(conn, 200) + + assert ^film_id = "#{film.id}" + end + end + + describe "film" do + test "returns film when it exists", %{conn: conn, film: film} do + query = """ + query { + film(id: #{film.id}) { + id + } + } + """ + + conn = get(conn, "/api/graphql", query: query) + + assert %{ + "data" => %{ + "film" => %{"id" => film_id} + } + } = json_response(conn, 200) + + assert ^film_id = "#{film.id}" + end + + test "loads nested fields", %{conn: conn, film: film} do + query = """ + query { + film(id: #{film.id}) { + id + species { + id + } + starships { + id + } + vehicles { + id + } + characters { + id + } + planets { + id + } + } + } + """ + + conn = get(conn, "/api/graphql", query: query) + + assert %{ + "data" => %{ + "film" => %{ + "id" => film_id, + "species" => [%{"id" => species_id}], + "starships" => [%{"id" => starship_id}], + "vehicles" => [%{"id" => vehicle_id}], + "characters" => [%{"id" => person_id}], + "planets" => [%{"id" => planet_id}] + } + } + } = json_response(conn, 200) + + assert ^film_id = "#{film.id}" + assert ^species_id = "#{List.first(film.species).id}" + assert ^starship_id = "#{List.first(film.starships).id}" + assert ^vehicle_id = "#{List.first(film.vehicles).id}" + assert ^person_id = "#{List.first(film.characters).id}" + assert ^planet_id = "#{List.first(film.planets).id}" + end + + test "handles recursive nesting", %{conn: conn, film: film} do + query = """ + query { + film(id: #{film.id}) { + id + characters { + id + films { + id + } + } + } + } + """ + + conn = get(conn, "/api/graphql", query: query) + + assert %{ + "data" => %{ + "film" => %{ + "id" => film_id, + "characters" => [%{"id" => person_id, "films" => [%{"id" => film_id}]}] + } + } + } = json_response(conn, 200) + + assert ^film_id = "#{film.id}" + assert ^person_id = "#{List.first(film.characters).id}" + end + end +end diff --git a/test/swapi_web/graphql/queries/person_queries_test.exs b/test/swapi_web/graphql/queries/person_queries_test.exs new file mode 100644 index 0000000..84679ee --- /dev/null +++ b/test/swapi_web/graphql/queries/person_queries_test.exs @@ -0,0 +1,182 @@ +defmodule SWAPIWeb.GraphQL.PersonQueriesTest do + use SWAPIWeb.ConnCase + + import Ecto.Changeset + + import SWAPI.FilmsFixtures + import SWAPI.PeopleFixtures + import SWAPI.PlanetsFixtures + import SWAPI.SpeciesFixtures + import SWAPI.StarshipsFixtures + import SWAPI.VehiclesFixtures + + alias SWAPI.Repo + alias SWAPI.Schemas.Person + + setup do + person = + person_fixture() + |> Person.changeset(%{name: "Luke Skywalker"}) + |> put_assoc(:homeworld, planet_fixture()) + |> put_assoc(:films, [film_fixture()]) + |> put_assoc(:species, [species_fixture()]) + |> put_assoc(:starships, [starship_fixture()]) + |> put_assoc(:vehicles, [vehicle_fixture()]) + |> Repo.update!() + + {:ok, %{person: person}} + end + + describe "allPeople" do + test "returns all people", %{conn: conn, person: person1} do + person2 = person_fixture() + + query = """ + query { + allPeople { + id + } + } + """ + + conn = get(conn, "/api/graphql", query: query) + + assert %{ + "data" => %{ + "allPeople" => [ + %{"id" => person1_id}, + %{"id" => person2_id} + ] + } + } = json_response(conn, 200) + + assert ^person1_id = "#{person1.id}" + assert ^person2_id = "#{person2.id}" + end + end + + describe "searchPeople" do + test "returns matching people", %{conn: conn, person: person} do + person_fixture(%{name: "Han Solo"}) + + query = """ + query { + searchPeople(searchTerms: ["Luke"]) { + id + } + } + """ + + conn = get(conn, "/api/graphql", query: query) + + assert %{ + "data" => %{ + "searchPeople" => [ + %{"id" => person_id} + ] + } + } = json_response(conn, 200) + + assert ^person_id = "#{person.id}" + end + end + + describe "person" do + test "returns person when it exists", %{conn: conn, person: person} do + query = """ + query { + person(id: #{person.id}) { + id + } + } + """ + + conn = get(conn, "/api/graphql", query: query) + + assert %{ + "data" => %{ + "person" => %{"id" => person_id} + } + } = json_response(conn, 200) + + assert ^person_id = "#{person.id}" + end + + test "loads nested fields", %{conn: conn, person: person} do + query = """ + query { + person(id: #{person.id}) { + id + homeworld { + id + } + films { + id + } + species { + id + } + starships { + id + } + vehicles { + id + } + } + } + """ + + conn = get(conn, "/api/graphql", query: query) + + assert %{ + "data" => %{ + "person" => %{ + "id" => person_id, + "homeworld" => %{"id" => homeworld_id}, + "films" => [%{"id" => film_id}], + "species" => [%{"id" => species_id}], + "starships" => [%{"id" => starship_id}], + "vehicles" => [%{"id" => vehicle_id}] + } + } + } = json_response(conn, 200) + + assert ^person_id = "#{person.id}" + assert ^homeworld_id = "#{person.homeworld.id}" + assert ^film_id = "#{List.first(person.films).id}" + assert ^species_id = "#{List.first(person.species).id}" + assert ^starship_id = "#{List.first(person.starships).id}" + assert ^vehicle_id = "#{List.first(person.vehicles).id}" + end + + test "handles recursive nesting", %{conn: conn, person: person} do + query = """ + query { + person(id: #{person.id}) { + id + films { + id + characters { + id + } + } + } + } + """ + + conn = get(conn, "/api/graphql", query: query) + + assert %{ + "data" => %{ + "person" => %{ + "id" => person_id, + "films" => [%{"id" => film_id, "characters" => [%{"id" => person_id}]}] + } + } + } = json_response(conn, 200) + + assert ^person_id = "#{person.id}" + assert ^film_id = "#{List.first(person.films).id}" + end + end +end diff --git a/test/swapi_web/graphql/queries/planet_queries_test.exs b/test/swapi_web/graphql/queries/planet_queries_test.exs new file mode 100644 index 0000000..1fb7ec6 --- /dev/null +++ b/test/swapi_web/graphql/queries/planet_queries_test.exs @@ -0,0 +1,161 @@ +defmodule SWAPIWeb.GraphQL.PlanetQueriesTest do + use SWAPIWeb.ConnCase + + import Ecto.Changeset + + import SWAPI.FilmsFixtures + import SWAPI.PeopleFixtures + import SWAPI.PlanetsFixtures + + alias SWAPI.Repo + alias SWAPI.Schemas.Planet + + setup do + planet = + planet_fixture() + |> Planet.changeset(%{name: "Tatooine"}) + |> put_assoc(:films, [film_fixture()]) + |> put_assoc(:residents, [person_fixture()]) + |> Repo.update!() + + {:ok, %{planet: planet}} + end + + describe "allPlanets" do + test "returns all planets", %{conn: conn, planet: planet1} do + planet2 = planet_fixture() + + query = """ + query { + allPlanets { + id + } + } + """ + + conn = get(conn, "/api/graphql", query: query) + + assert %{ + "data" => %{ + "allPlanets" => [ + %{"id" => planet1_id}, + %{"id" => planet2_id} + ] + } + } = json_response(conn, 200) + + assert ^planet1_id = "#{planet1.id}" + assert ^planet2_id = "#{planet2.id}" + end + end + + describe "searchPlanets" do + test "returns matching planets", %{conn: conn, planet: planet} do + planet_fixture(%{name: "Alderaan"}) + + query = """ + query { + searchPlanets(searchTerms: ["Tatooine"]) { + id + } + } + """ + + conn = get(conn, "/api/graphql", query: query) + + assert %{ + "data" => %{ + "searchPlanets" => [ + %{"id" => planet_id} + ] + } + } = json_response(conn, 200) + + assert ^planet_id = "#{planet.id}" + end + end + + describe "planet" do + test "returns planet when it exists", %{conn: conn, planet: planet} do + query = """ + query { + planet(id: #{planet.id}) { + id + } + } + """ + + conn = get(conn, "/api/graphql", query: query) + + assert %{ + "data" => %{ + "planet" => %{"id" => planet_id} + } + } = json_response(conn, 200) + + assert ^planet_id = "#{planet.id}" + end + + test "loads nested fields", %{conn: conn, planet: planet} do + query = """ + query { + planet(id: #{planet.id}) { + id + films { + id + } + residents { + id + } + } + } + """ + + conn = get(conn, "/api/graphql", query: query) + + assert %{ + "data" => %{ + "planet" => %{ + "id" => planet_id, + "films" => [%{"id" => film_id}], + "residents" => [%{"id" => resident_id}] + } + } + } = json_response(conn, 200) + + assert ^planet_id = "#{planet.id}" + assert ^film_id = "#{List.first(planet.films).id}" + assert ^resident_id = "#{List.first(planet.residents).id}" + end + + test "handles recursive nesting", %{conn: conn, planet: planet} do + query = """ + query { + planet(id: #{planet.id}) { + id + residents { + id + homeworld { + id + } + } + } + } + """ + + conn = get(conn, "/api/graphql", query: query) + + assert %{ + "data" => %{ + "planet" => %{ + "id" => planet_id, + "residents" => [%{"id" => resident_id, "homeworld" => %{"id" => planet_id}}] + } + } + } = json_response(conn, 200) + + assert ^planet_id = "#{planet.id}" + assert ^resident_id = "#{List.first(planet.residents).id}" + end + end +end diff --git a/test/swapi_web/graphql/queries/species_queries_test.exs b/test/swapi_web/graphql/queries/species_queries_test.exs new file mode 100644 index 0000000..a50a139 --- /dev/null +++ b/test/swapi_web/graphql/queries/species_queries_test.exs @@ -0,0 +1,168 @@ +defmodule SWAPIWeb.GraphQL.SpeciesQueriesTest do + use SWAPIWeb.ConnCase + + import Ecto.Changeset + + import SWAPI.FilmsFixtures + import SWAPI.PeopleFixtures + import SWAPI.PlanetsFixtures + import SWAPI.SpeciesFixtures + + alias SWAPI.Repo + alias SWAPI.Schemas.Species + + setup do + species = + species_fixture() + |> Species.changeset(%{name: "Human"}) + |> put_assoc(:homeworld, planet_fixture()) + |> put_assoc(:films, [film_fixture()]) + |> put_assoc(:people, [person_fixture()]) + |> Repo.update!() + + {:ok, %{species: species}} + end + + describe "allSpecies" do + test "returns all species", %{conn: conn, species: species1} do + species2 = species_fixture() + + query = """ + query { + allSpecies { + id + } + } + """ + + conn = get(conn, "/api/graphql", query: query) + + assert %{ + "data" => %{ + "allSpecies" => [ + %{"id" => species1_id}, + %{"id" => species2_id} + ] + } + } = json_response(conn, 200) + + assert ^species1_id = "#{species1.id}" + assert ^species2_id = "#{species2.id}" + end + end + + describe "searchSpecies" do + test "returns matching species", %{conn: conn, species: species} do + species_fixture(%{name: "Wookiee"}) + + query = """ + query { + searchSpecies(searchTerms: ["Human"]) { + id + } + } + """ + + conn = get(conn, "/api/graphql", query: query) + + assert %{ + "data" => %{ + "searchSpecies" => [ + %{"id" => species_id} + ] + } + } = json_response(conn, 200) + + assert ^species_id = "#{species.id}" + end + end + + describe "species" do + test "returns species when it exists", %{conn: conn, species: species} do + query = """ + query { + species(id: #{species.id}) { + id + } + } + """ + + conn = get(conn, "/api/graphql", query: query) + + assert %{ + "data" => %{ + "species" => %{"id" => species_id} + } + } = json_response(conn, 200) + + assert ^species_id = "#{species.id}" + end + + test "loads nested fields", %{conn: conn, species: species} do + query = """ + query { + species(id: #{species.id}) { + id + homeworld { + id + } + films { + id + } + people { + id + } + } + } + """ + + conn = get(conn, "/api/graphql", query: query) + + assert %{ + "data" => %{ + "species" => %{ + "id" => species_id, + "homeworld" => %{"id" => homeworld_id}, + "films" => [%{"id" => film_id}], + "people" => [%{"id" => person_id}] + } + } + } = json_response(conn, 200) + + assert ^species_id = "#{species.id}" + assert ^homeworld_id = "#{species.homeworld.id}" + assert ^film_id = "#{List.first(species.films).id}" + assert ^person_id = "#{List.first(species.people).id}" + end + + test "handles recursive nesting", %{conn: conn, species: species} do + query = """ + query { + species(id: #{species.id}) { + id + people { + id + species { + id + } + } + } + } + """ + + conn = get(conn, "/api/graphql", query: query) + + assert %{ + "data" => %{ + "species" => %{ + "id" => species_id, + "people" => [%{"id" => person_id, "species" => [%{"id" => species_id}]}] + } + } + } = json_response(conn, 200) + + assert ^species_id = "#{species.id}" + assert ^person_id = "#{List.first(species.people).id}" + end + end +end diff --git a/test/swapi_web/graphql/queries/starship_queries_test.exs b/test/swapi_web/graphql/queries/starship_queries_test.exs new file mode 100644 index 0000000..b675945 --- /dev/null +++ b/test/swapi_web/graphql/queries/starship_queries_test.exs @@ -0,0 +1,161 @@ +defmodule SWAPIWeb.GraphQL.StarshipQueriesTest do + use SWAPIWeb.ConnCase + + import Ecto.Changeset + + import SWAPI.FilmsFixtures + import SWAPI.PeopleFixtures + import SWAPI.StarshipsFixtures + + alias SWAPI.Repo + alias SWAPI.Schemas.Starship + + setup do + starship = + starship_fixture() + |> Starship.changeset(%{transport: %{name: "X-wing"}}) + |> put_assoc(:films, [film_fixture()]) + |> put_assoc(:pilots, [person_fixture()]) + |> Repo.update!() + + {:ok, %{starship: starship}} + end + + describe "allStarships" do + test "returns all starships", %{conn: conn, starship: starship1} do + starship2 = starship_fixture() + + query = """ + query { + allStarships { + id + } + } + """ + + conn = get(conn, "/api/graphql", query: query) + + assert %{ + "data" => %{ + "allStarships" => [ + %{"id" => starship1_id}, + %{"id" => starship2_id} + ] + } + } = json_response(conn, 200) + + assert ^starship1_id = "#{starship1.id}" + assert ^starship2_id = "#{starship2.id}" + end + end + + describe "searchStarships" do + test "returns matching starships", %{conn: conn, starship: starship} do + starship_fixture(%{name: "Millennium Falcon"}) + + query = """ + query { + searchStarships(searchTerms: ["X-wing"]) { + id + } + } + """ + + conn = get(conn, "/api/graphql", query: query) + + assert %{ + "data" => %{ + "searchStarships" => [ + %{"id" => starship_id} + ] + } + } = json_response(conn, 200) + + assert ^starship_id = "#{starship.id}" + end + end + + describe "starship" do + test "returns starship when it exists", %{conn: conn, starship: starship} do + query = """ + query { + starship(id: #{starship.id}) { + id + } + } + """ + + conn = get(conn, "/api/graphql", query: query) + + assert %{ + "data" => %{ + "starship" => %{"id" => starship_id} + } + } = json_response(conn, 200) + + assert ^starship_id = "#{starship.id}" + end + + test "loads nested fields", %{conn: conn, starship: starship} do + query = """ + query { + starship(id: #{starship.id}) { + id + films { + id + } + pilots { + id + } + } + } + """ + + conn = get(conn, "/api/graphql", query: query) + + assert %{ + "data" => %{ + "starship" => %{ + "id" => starship_id, + "films" => [%{"id" => film_id}], + "pilots" => [%{"id" => pilot_id}] + } + } + } = json_response(conn, 200) + + assert ^starship_id = "#{starship.id}" + assert ^film_id = "#{List.first(starship.films).id}" + assert ^pilot_id = "#{List.first(starship.pilots).id}" + end + + test "handles recursive nesting", %{conn: conn, starship: starship} do + query = """ + query { + starship(id: #{starship.id}) { + id + pilots { + id + starships { + id + } + } + } + } + """ + + conn = get(conn, "/api/graphql", query: query) + + assert %{ + "data" => %{ + "starship" => %{ + "id" => starship_id, + "pilots" => [%{"id" => pilot_id, "starships" => [%{"id" => starship_id}]}] + } + } + } = json_response(conn, 200) + + assert ^starship_id = "#{starship.id}" + assert ^pilot_id = "#{List.first(starship.pilots).id}" + end + end +end diff --git a/test/swapi_web/graphql/queries/vehicle_queries_test.exs b/test/swapi_web/graphql/queries/vehicle_queries_test.exs new file mode 100644 index 0000000..0d45d2d --- /dev/null +++ b/test/swapi_web/graphql/queries/vehicle_queries_test.exs @@ -0,0 +1,161 @@ +defmodule SWAPIWeb.GraphQL.VehicleQueriesTest do + use SWAPIWeb.ConnCase + + import Ecto.Changeset + + import SWAPI.FilmsFixtures + import SWAPI.PeopleFixtures + import SWAPI.VehiclesFixtures + + alias SWAPI.Repo + alias SWAPI.Schemas.Vehicle + + setup do + vehicle = + vehicle_fixture() + |> Vehicle.changeset(%{transport: %{name: "Snowspeeder"}}) + |> put_assoc(:films, [film_fixture()]) + |> put_assoc(:pilots, [person_fixture()]) + |> Repo.update!() + + {:ok, %{vehicle: vehicle}} + end + + describe "allVehicles" do + test "returns all vehicles", %{conn: conn, vehicle: vehicle1} do + vehicle2 = vehicle_fixture() + + query = """ + query { + allVehicles { + id + } + } + """ + + conn = get(conn, "/api/graphql", query: query) + + assert %{ + "data" => %{ + "allVehicles" => [ + %{"id" => vehicle1_id}, + %{"id" => vehicle2_id} + ] + } + } = json_response(conn, 200) + + assert ^vehicle1_id = "#{vehicle1.id}" + assert ^vehicle2_id = "#{vehicle2.id}" + end + end + + describe "searchVehicles" do + test "returns matching vehicles", %{conn: conn, vehicle: vehicle} do + vehicle_fixture(%{name: "AT-AT"}) + + query = """ + query { + searchVehicles(searchTerms: ["Snowspeeder"]) { + id + } + } + """ + + conn = get(conn, "/api/graphql", query: query) + + assert %{ + "data" => %{ + "searchVehicles" => [ + %{"id" => vehicle_id} + ] + } + } = json_response(conn, 200) + + assert ^vehicle_id = "#{vehicle.id}" + end + end + + describe "vehicle" do + test "returns vehicle when it exists", %{conn: conn, vehicle: vehicle} do + query = """ + query { + vehicle(id: #{vehicle.id}) { + id + } + } + """ + + conn = get(conn, "/api/graphql", query: query) + + assert %{ + "data" => %{ + "vehicle" => %{"id" => vehicle_id} + } + } = json_response(conn, 200) + + assert ^vehicle_id = "#{vehicle.id}" + end + + test "loads nested fields", %{conn: conn, vehicle: vehicle} do + query = """ + query { + vehicle(id: #{vehicle.id}) { + id + films { + id + } + pilots { + id + } + } + } + """ + + conn = get(conn, "/api/graphql", query: query) + + assert %{ + "data" => %{ + "vehicle" => %{ + "id" => vehicle_id, + "films" => [%{"id" => film_id}], + "pilots" => [%{"id" => pilot_id}] + } + } + } = json_response(conn, 200) + + assert ^vehicle_id = "#{vehicle.id}" + assert ^film_id = "#{List.first(vehicle.films).id}" + assert ^pilot_id = "#{List.first(vehicle.pilots).id}" + end + + test "handles recursive nesting", %{conn: conn, vehicle: vehicle} do + query = """ + query { + vehicle(id: #{vehicle.id}) { + id + pilots { + id + vehicles { + id + } + } + } + } + """ + + conn = get(conn, "/api/graphql", query: query) + + assert %{ + "data" => %{ + "vehicle" => %{ + "id" => vehicle_id, + "pilots" => [%{"id" => pilot_id, "vehicles" => [%{"id" => vehicle_id}]}] + } + } + } = json_response(conn, 200) + + assert ^vehicle_id = "#{vehicle.id}" + assert ^pilot_id = "#{List.first(vehicle.pilots).id}" + end + end +end diff --git a/test/swapi_web/graphql/resolvers/film_resolver_test.exs b/test/swapi_web/graphql/resolvers/film_resolver_test.exs new file mode 100644 index 0000000..46c1912 --- /dev/null +++ b/test/swapi_web/graphql/resolvers/film_resolver_test.exs @@ -0,0 +1,43 @@ +defmodule SWAPIWeb.GraphQL.Resolvers.FilmResolverTest do + use SWAPI.DataCase + + alias SWAPIWeb.GraphQL.Resolvers.FilmResolver + import SWAPI.FilmsFixtures + + describe "all/2" do + test "returns all films" do + %{id: film_id} = film_fixture() + assert {:ok, [%{id: ^film_id}]} = FilmResolver.all(%{}, %{}) + end + + test "returns empty list when no films exist" do + assert {:ok, []} = FilmResolver.all(%{}, %{}) + end + end + + describe "one/2" do + test "returns film when it exists" do + %{id: film_id} = film_fixture() + assert {:ok, %{id: ^film_id}} = FilmResolver.one(%{id: film_id}, %{}) + end + + test "returns error when film doesn't exist" do + assert {:error, "Film not found"} = FilmResolver.one(%{id: 0}, %{}) + end + end + + describe "search/2" do + test "returns matching films" do + %{id: film_id} = film_fixture(%{title: "A New Hope"}) + film_fixture(%{title: "Empire Strikes Back"}) + + assert {:ok, [%{id: ^film_id}]} = FilmResolver.search(%{search_terms: ["Hope"]}, %{}) + end + + test "returns empty list when no matches found" do + film_fixture(%{title: "A New Hope"}) + + assert {:ok, []} = FilmResolver.search(%{search_terms: ["Non-existent"]}, %{}) + end + end +end diff --git a/test/swapi_web/graphql/resolvers/person_resolver_test.exs b/test/swapi_web/graphql/resolvers/person_resolver_test.exs new file mode 100644 index 0000000..03222d7 --- /dev/null +++ b/test/swapi_web/graphql/resolvers/person_resolver_test.exs @@ -0,0 +1,43 @@ +defmodule SWAPIWeb.GraphQL.Resolvers.PersonResolverTest do + use SWAPI.DataCase + + alias SWAPIWeb.GraphQL.Resolvers.PersonResolver + import SWAPI.PeopleFixtures + + describe "all/2" do + test "returns all people" do + %{id: person_id} = person_fixture() + assert {:ok, [%{id: ^person_id}]} = PersonResolver.all(%{}, %{}) + end + + test "returns empty list when no people exist" do + assert {:ok, []} = PersonResolver.all(%{}, %{}) + end + end + + describe "one/2" do + test "returns person when they exist" do + %{id: person_id} = person_fixture() + assert {:ok, %{id: ^person_id}} = PersonResolver.one(%{id: person_id}, %{}) + end + + test "returns error when person doesn't exist" do + assert {:error, "Person not found"} = PersonResolver.one(%{id: 0}, %{}) + end + end + + describe "search/2" do + test "returns matching people" do + %{id: person_id} = person_fixture(%{name: "Luke Skywalker"}) + person_fixture(%{name: "Leia Organa"}) + + assert {:ok, [%{id: ^person_id}]} = PersonResolver.search(%{search_terms: ["Luke"]}, %{}) + end + + test "returns empty list when no matches found" do + person_fixture(%{name: "Luke Skywalker"}) + + assert {:ok, []} = PersonResolver.search(%{search_terms: ["Non-existent"]}, %{}) + end + end +end diff --git a/test/swapi_web/graphql/resolvers/planet_resolver_test.exs b/test/swapi_web/graphql/resolvers/planet_resolver_test.exs new file mode 100644 index 0000000..51491d2 --- /dev/null +++ b/test/swapi_web/graphql/resolvers/planet_resolver_test.exs @@ -0,0 +1,44 @@ +defmodule SWAPIWeb.GraphQL.Resolvers.PlanetResolverTest do + use SWAPI.DataCase + + alias SWAPIWeb.GraphQL.Resolvers.PlanetResolver + import SWAPI.PlanetsFixtures + + describe "all/2" do + test "returns all planets" do + %{id: planet_id} = planet_fixture() + assert {:ok, [%{id: ^planet_id}]} = PlanetResolver.all(%{}, %{}) + end + + test "returns empty list when no planets exist" do + assert {:ok, []} = PlanetResolver.all(%{}, %{}) + end + end + + describe "one/2" do + test "returns planet when it exists" do + %{id: planet_id} = planet_fixture() + assert {:ok, %{id: ^planet_id}} = PlanetResolver.one(%{id: planet_id}, %{}) + end + + test "returns error when planet doesn't exist" do + assert {:error, "Planet not found"} = PlanetResolver.one(%{id: 0}, %{}) + end + end + + describe "search/2" do + test "returns matching planets" do + %{id: planet_id} = planet_fixture(%{name: "Tatooine"}) + planet_fixture(%{name: "Alderaan"}) + + assert {:ok, [%{id: ^planet_id}]} = + PlanetResolver.search(%{search_terms: ["Tatooine"]}, %{}) + end + + test "returns empty list when no matches found" do + planet_fixture(%{name: "Tatooine"}) + + assert {:ok, []} = PlanetResolver.search(%{search_terms: ["Non-existent"]}, %{}) + end + end +end diff --git a/test/swapi_web/graphql/resolvers/species_resolver_test.exs b/test/swapi_web/graphql/resolvers/species_resolver_test.exs new file mode 100644 index 0000000..aeb5209 --- /dev/null +++ b/test/swapi_web/graphql/resolvers/species_resolver_test.exs @@ -0,0 +1,43 @@ +defmodule SWAPIWeb.GraphQL.Resolvers.SpeciesResolverTest do + use SWAPI.DataCase + + alias SWAPIWeb.GraphQL.Resolvers.SpeciesResolver + import SWAPI.SpeciesFixtures + + describe "all/2" do + test "returns all species" do + %{id: species_id} = species_fixture() + assert {:ok, [%{id: ^species_id}]} = SpeciesResolver.all(%{}, %{}) + end + + test "returns empty list when no species exist" do + assert {:ok, []} = SpeciesResolver.all(%{}, %{}) + end + end + + describe "one/2" do + test "returns species when it exists" do + %{id: species_id} = species_fixture() + assert {:ok, %{id: ^species_id}} = SpeciesResolver.one(%{id: species_id}, %{}) + end + + test "returns error when species doesn't exist" do + assert {:error, "Species not found"} = SpeciesResolver.one(%{id: 0}, %{}) + end + end + + describe "search/2" do + test "returns matching species" do + %{id: species_id} = species_fixture(%{name: "Human"}) + species_fixture(%{name: "Wookiee"}) + + assert {:ok, [%{id: ^species_id}]} = SpeciesResolver.search(%{search_terms: ["Human"]}, %{}) + end + + test "returns empty list when no matches found" do + species_fixture(%{name: "Human"}) + + assert {:ok, []} = SpeciesResolver.search(%{search_terms: ["Non-existent"]}, %{}) + end + end +end diff --git a/test/swapi_web/graphql/resolvers/starship_resolver_test.exs b/test/swapi_web/graphql/resolvers/starship_resolver_test.exs new file mode 100644 index 0000000..0a77b1c --- /dev/null +++ b/test/swapi_web/graphql/resolvers/starship_resolver_test.exs @@ -0,0 +1,44 @@ +defmodule SWAPIWeb.GraphQL.Resolvers.StarshipResolverTest do + use SWAPI.DataCase + + alias SWAPIWeb.GraphQL.Resolvers.StarshipResolver + import SWAPI.StarshipsFixtures + + describe "all/2" do + test "returns all starships" do + %{id: starship_id} = starship_fixture() + assert {:ok, [%{id: ^starship_id}]} = StarshipResolver.all(%{}, %{}) + end + + test "returns empty list when no starships exist" do + assert {:ok, []} = StarshipResolver.all(%{}, %{}) + end + end + + describe "one/2" do + test "returns starship when it exists" do + %{id: starship_id} = starship_fixture() + assert {:ok, %{id: ^starship_id}} = StarshipResolver.one(%{id: starship_id}, %{}) + end + + test "returns error when starship doesn't exist" do + assert {:error, "Starship not found"} = StarshipResolver.one(%{id: 0}, %{}) + end + end + + describe "search/2" do + test "returns matching starships" do + %{id: starship_id} = starship_fixture(%{transport: %{name: "X-wing"}}) + starship_fixture(%{transport: %{name: "TIE Fighter"}}) + + assert {:ok, [%{id: ^starship_id}]} = + StarshipResolver.search(%{search_terms: ["X-wing"]}, %{}) + end + + test "returns empty list when no matches found" do + starship_fixture(%{transport: %{name: "X-wing"}}) + + assert {:ok, []} = StarshipResolver.search(%{search_terms: ["Non-existent"]}, %{}) + end + end +end diff --git a/test/swapi_web/graphql/resolvers/vehicle_resolver_test.exs b/test/swapi_web/graphql/resolvers/vehicle_resolver_test.exs new file mode 100644 index 0000000..18a7122 --- /dev/null +++ b/test/swapi_web/graphql/resolvers/vehicle_resolver_test.exs @@ -0,0 +1,43 @@ +defmodule SWAPIWeb.GraphQL.Resolvers.VehicleResolverTest do + use SWAPI.DataCase + + alias SWAPIWeb.GraphQL.Resolvers.VehicleResolver + import SWAPI.VehiclesFixtures + + describe "all/2" do + test "returns all vehicles" do + %{id: vehicle_id} = vehicle_fixture() + assert {:ok, [%{id: ^vehicle_id}]} = VehicleResolver.all(%{}, %{}) + end + + test "returns empty list when no vehicles exist" do + assert {:ok, []} = VehicleResolver.all(%{}, %{}) + end + end + + describe "one/2" do + test "returns vehicle when it exists" do + %{id: vehicle_id} = vehicle_fixture() + assert {:ok, %{id: ^vehicle_id}} = VehicleResolver.one(%{id: vehicle_id}, %{}) + end + + test "returns error when vehicle doesn't exist" do + assert {:error, "Vehicle not found"} = VehicleResolver.one(%{id: 0}, %{}) + end + end + + describe "search/2" do + test "returns matching vehicles" do + %{id: vehicle_id} = vehicle_fixture(%{transport: %{name: "AT-AT"}}) + vehicle_fixture(%{name: "Snowspeeder"}) + + assert {:ok, [%{id: ^vehicle_id}]} = VehicleResolver.search(%{search_terms: ["AT-AT"]}, %{}) + end + + test "returns empty list when no matches found" do + vehicle_fixture(%{transport: %{name: "AT-AT"}}) + + assert {:ok, []} = VehicleResolver.search(%{search_terms: ["Non-existent"]}, %{}) + end + end +end