Erstellen einer JSON-API mit Phoenix 1.3 und Elixir – Notizen von dead_wolf

Originalveröffentlichung unter: Notizen von wolf_tuerto

Hast du ein Schienen Hintergrund?

Bist du es leid, zuzusehen veraltet oder unvollständig Tutorials, wie es geht
Bau ein JSON-API verwenden Elixier und Phönix?

Dann, weiter lesen mein Freund!

Ich habe festgestellt, dass es hauptsächlich zwei Arten von Tutorials gibt, die man schreiben sollte:

  • Gezielte, fokussierte Tutorials.
  • Vollständige Schritt-für-Schritt-Tutorials.

Umfassende, fokussierte Tutorials sollten verwendet werden, um Techniken wie diese zu erklären:
Flüssige SVGs mit Vue.js.

Es sollten jedoch vollständige Schritt-für-Schritt-Tutorials verwendet werden, um etwas über neue Tech-Stacks zu lernen.

Gehen von Null zu voll funktionsfähiger Prototyp ohne Schritte zu überspringen.
Mit integrierten Best Practices, die die besten verfügbaren Bibliotheken präsentieren
für eine gegebene Aufgabe.
Ich mag wirklich Tutorials, die dies übernehmen ganzheitlich sich nähern.

Es geht also nicht nur darum, wie man einen neuen Phönix generiert Nur API App.
Das ist ganz einfach, Sie müssen nur die passieren --no-brunch --no-html
zu mix phx.new.

In diesem Tutorial geht es darum, eine kleine, aber voll funktionsfähige JSON-API für zu erstellen
Web Applikationen.


Zur Ergänzung Ihres API, Ich empfehle Vue.js am Frontend:
Schnellstartanleitung für ein neues Vue.js-Projekt.


Was wir tun werden:

  • Erstellen Sie eine neue reine API-Phoenix-Anwendung – überspringen Sie HTML- und JS-Zeug.
  • Erstellen Sie ein Benutzerschemamodul und hashen Sie sein Passwort, weil es Klartext speichert
    Passwörter in der Datenbank ist nur falsch.
  • Erstellen Sie einen Benutzer-Endpunkt – damit Sie eine Liste von Benutzern erhalten, erstellen oder löschen können!
  • CORS-Konfiguration – damit Sie Ihr Frontend verwenden können, auf dem es ausgeführt wird
    einen anderen Port / eine andere Domäne.
  • Erstellen Sie einen Anmeldeendpunkt – mit sitzungsbasierter Authentifizierung über Cookies.

Wenn Sie daran interessiert sind, auth mit zu machen JWTs, überprüfen dieses andere Tutorial aus.


Lassen Sie mich etwas klarstellen…

Ich fange gerade erst an Elixier / Phönix, wenn es Auslassungen oder schlechte Praktiken gibt,
Geduld mit mir, benachrichtigen Sie mich und ich werde sie so schnell wie möglich beheben.

Dies ist das Tutorial, von dem ich wünschte, ich hätte es zur Verfügung gehabt, als ich versuchte, zu lernen, wie es geht
implementieren a JSON-API mit Elixir / Phoenix … Aber ich schweife ab.


Elixier installieren

Wir beginnen mit der Installation Erlang und Elixier Verwendung der asdf Ausführung
manager —Die Verwendung von Versionsmanagern ist a beste Übung in Entwicklungsumgebungen.

Installieren Sie PostgreSQL

PostgreSQL ist die Standarddatenbank für neue Phoenix-Apps, und das aus gutem Grund:
Es ist eine solide, zuverlässige und ausgereifte relationale DB.

Über REST-Clients

Möglicherweise müssen Sie eine bekommen REST-Client damit du es ausprobieren kannst API Endpunkte.

Die beiden beliebtesten scheinen zu sein Postbote und Erweiterter Rest-Client ich
habe beide getestet und ich kann sagen, dass sie beide nicht mochten – zumindest in ihrer Chrome-App
Inkarnationen — da eine keine Cookie-Informationen anzeigte und die andere nicht sendete
deklarierte Variablen bei POST-Anforderungen. ¬¬

Auf jeden Fall, wenn Sie sie ausprobieren möchten:

  • Sie können Postman bekommen hier.
  • Sie können ARC erhalten hier.

Wenn Sie eine Web-Frontend-Bibliothek wie z Axiosdann deine
Die Entwicklertools des Browsers sollten ausreichen:

Wenn du mit gehst Axios Vergessen Sie nicht, die Konfigurationsoption zu übergeben withCredentials: true,
Dadurch kann der Client bei CORS-Anforderungen Cookies mitsenden.

Oder du kannst einfach das gute alte verwenden curl es funktioniert wirklich gut!
Ich zeige Ihnen einige Beispiele, wie Sie Ihre Endpunkte über die CLI testen können.


Generieren Sie die App-Dateien

In Ihrem Terminal:

mix phx.new my-app --app my_app --module MyApp --no-brunch --no-html

Aus dem obigen Befehl:

  • Du wirst sehen my-app als Name für das Verzeichnis, das für diese Anwendung erstellt wurde.

  • Du wirst sehen my_app in Dateien und Verzeichnissen darin verwendet my-app/lib z.B my_app.ex.

  • Du wirst es finden MyApp überall verwendet, da es das Hauptmodul für Ihre App ist.

    Zum Beispiel im my-app/lib/my_app.ex:

    defmodule MyApp do
      @moduledoc """
      MyApp keeps the contexts that define your domain
      and business logic.
    
      Contexts are also responsible for managing your data, regardless
      if it comes from the database, an external API or others.
      """
    end
    

Erstellen Sie die Entwicklungsdatenbank

Wenn Sie bei der Installation einen neuen DB-Benutzer erstellt haben PostgreSQLfügen Sie seine Anmeldeinformationen hinzu
config/dev.exs und config/test.exs. Dann ausführen:

cd my-app
mix ecto.create

HINWEIS:

Sie können die Datenbank für die löschen Entwickler Umfeld mit:

mix ecto.drop

Wenn Sie die Datenbank für die löschen möchten Prüfung Umgebung, müssten Sie:

MIX_ENV=test mix ecto.drop

Starten Sie den Entwicklungsserver

Von Ihrem Endgerät:

mix phx.server

Besuch und sonnen Sie sich im Glanz einer wunderschön formatierten Fehlerseite. 😃
Keine Sorge, wir werden bald genug einen JSON-Endpunkt hinzufügen.

Router-Fehler

Fehler in JSON für 404s und 500s

Wenn Sie keine HTML-Seiten sehen möchten, wenn ein Fehler auftritt, und stattdessen empfangen möchten
JSONs, festgelegt debug_errors zu false in deiner config/dev.exund starten Sie Ihren Server neu:

config :my_app, MyAppWeb.Endpoint,
  
  debug_errors: false,
  

Jetzt zu Besuch Erträge:

{ "errors": { "detail": "Not Found" } }

Um den Entwicklungsserver zu stoppen, klicken Sie auf CTRL+C zweimal.


Wir werden eine neue generieren Benutzer Schema in einem Auth Kontext.

Kontexte in Phoenix sind cool, sie dienen als API-Grenzen, mit denen Sie
Organisieren Sie Ihren Anwendungscode besser.

Generieren Sie die Benutzerschema- und Auth-Kontextmodule

mix phx.gen.context Auth User users email:string:unique \
is_active:boolean

Von oben:

  • Auth ist der Modulname des Kontexts.
  • Benutzer ist der Modulname des Schemas.
  • Benutzer ist der Name der DB-Tabelle.
  • Danach kommen einige Felddefinitionen.

Öffnen Sie die aus dem vorherigen Befehl generierte Migrationsdatei in
priv/repo/migrations/<some time stamp>_create_users.exs und nehmen wir einige Änderungen daran vor:

  • Email kann nicht null sein.
  • Füge hinzu ein Passwort_hash String-Feld.
defmodule MyApp.Repo.Migrations.CreateUsers do
  use Ecto.Migration

  def change do
    create table(:users) do
      add(:email, :string, null: false)
      add(:password_hash, :string)
      add(:is_active, :boolean, default: false, null: false)

      timestamps()
    end

    create(unique_index(:users, [:email]))
  end
end

Führen Sie die neue Migration aus:

mix ecto.migrate

Wenn Sie einige Informationen zu diesem Generator lesen möchten, führen Sie Folgendes aus:

mix help phx.gen.context

Hash eines Benutzerpassworts beim Speichern

Fügen Sie eine neue Abhängigkeit hinzu mix.exs:

  defp deps do
    [
      
      {:bcrypt_elixir, "~> 1.0"}
    ]
  end

Das ist Bcryptverwenden wir es, um das Passwort des Benutzers zu hashen, bevor wir es speichern;
Daher speichern wir es nicht als einfachen Text in der Datenbank.

Rufen Sie die neuen App-Abhängigkeiten ab mit:

mix deps.get

Fügen Sie außerdem die nächste Zeile am Ende von hinzu config/test.exs:

config :bcrypt_elixir, :log_rounds, 4

Nicht hinzufügen diese Konfigurationsoption zu config/dev.exs oder config/prod.exs!
Es wird nur während verwendet testen zu beschleunigen den Prozess durch Verringern der Sicherheitseinstellungen in dieser Umgebung.

Fügen wir unserem Benutzerschema ein virtuelles Feld hinzu — virtuell, was bedeutet, dass es keinen Platz in unserer Datenbank hat.
Veränderung lib/my_app/auth/user.ex so aussehen:

defmodule MyApp.Auth.User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field(:email, :string)
    field(:is_active, :boolean, default: false)
    field(:password, :string, virtual: true)
    field(:password_hash, :string)

    timestamps()
  end

  @doc false
  def changeset(user, attrs) do
    user
    |> cast(attrs, [:email, :is_active, :password])
    |> validate_required([:email, :is_active, :password])
    |> unique_constraint(:email)
    |> put_password_hash()
  end

  defp put_password_hash(
         %Ecto.Changeset{
           valid?: true, changes: %{password: password}
         } = changeset
       ) do
    change(changeset, password_hash: Bcrypt.hash_pwd_salt(password))
  end

  defp put_password_hash(changeset) do
    changeset
  end
end

Beachten Sie den Aufruf und die Definitionen von put_password_hash/1.

Was dies tut, ist das Ausführen der Änderungssatz durch diese Funktion, und wenn die
changeset hat zufällig eine password Schlüssel, wird es verwenden Bcrypt zu
hat geschlagen.


Betrieb Bcrypt.hash_pwd_salt("hola") würde so etwas ergeben wie:

"$2b$12$sI3PE3UsOE0BPrUv7TwUt.i4BQ32kxgK.REDv.IHC8HlEVAkqmHky"

Diese seltsam aussehende Zeichenfolge wird stattdessen in der Datenbank gespeichert
der Klartextversion.


Korrigieren Sie die Tests

Führen Sie die Tests für Ihr Projekt aus mit:

mix test

Im Moment werden sie scheitern mit:

  1) test users create_user/1 with valid data creates a user (MyApp.AuthTest)
     test/my_app/auth/auth_test.exs:32
     match (=) failed
     code:  assert {:ok, %User{} = user} = Auth.create_user(@valid_attrs)
     right: {:error,
             
               action: :insert,
               changes: %{email: "some email", is_active: true},
               errors: [password: {"can't be blank", [validation: :required]}],
               data: 
               valid?: false
             >}
     stacktrace:
       test/my_app/auth/auth_test.exs:33: (test)

Das liegt an den Änderungen, die wir gerade am Benutzerschema vorgenommen haben.

Dies lässt sich jedoch leicht beheben, indem Sie die hinzufügen password Attribut, wo es benötigt wird test/my_app/auth/auth_test.exs:

defmodule MyApp.AuthTest do
  
  describe "users" do
    
    @valid_attrs %{email: "some email", is_active: true, password: "some password"}
    @update_attrs %{
      email: "some updated email",
      is_active: false,
      password: "some updated password"
    }
    @invalid_attrs %{email: nil, is_active: nil, password: nil}
    
  end
end

Versuchen wir es mit mix test wieder:

    1) Prüfung users list_users/0 gibt alle Benutzer zurück (MyApp.AuthTest)
     Prüfung/my_app/auth/auth_test.exs:26 Assertion mit == fehlgeschlagenem Code: assert Auth.list_users() == [user]
     links:  [%MyApp.Auth.User{__meta__: 
     right: [%MyApp.Auth.User{__meta__: 
     stacktrace:
       test/my_app/auth/auth_test.exs:28: (test)



  2) test users update_user/2 with invalid data returns error changeset (MyApp.AuthTest)
     test/my_app/auth/auth_test.exs:54
     Assertion with == failed
     code:  assert user == Auth.get_user!(user.id())
     left:  %MyApp.Auth.User{__meta__: 
     right: %MyApp.Auth.User{__meta__: 
     stacktrace:
       test/my_app/auth/auth_test.exs:57: (test)

..

  3) test users get_user!/1 returns the user with given id (MyApp.AuthTest)
     test/my_app/auth/auth_test.exs:31
     Assertion with == failed
     code:  assert Auth.get_user!(user.id()) == user
     left:  %MyApp.Auth.User{__meta__: 
     right: %MyApp.Auth.User{__meta__: 
     stacktrace:
       test/my_app/auth/auth_test.exs:33: (test)

Here the problem is that when we get a user from the DB, password is going to be nil, since we are
only using that field to create or update a user.

To fix that, we will assign nil to user.password and while fixing those, let’s take the opportunity
to test the password verification on create_user and update_user too:

defmodule MyApp.AuthTest do
  
  describe "users" do
    
    test "list_users/0 returns all users" do
      
      assert Auth.list_users() == [%User{user | password: nil}]
    Ende

    Prüfung "get_user!/1 gibt den Benutzer mit der angegebenen ID zurück" tun
      
      behaupten Auth.get_user!(user.id) == %User{user | Passwort: Null}
    Ende

    Prüfung "create_user/1 mit gültigen Daten erstellt einen Benutzer" tun
      
      behaupten Sie Bcrypt.verify_pass ("irgendein Passwort"user.password_hash)
    Ende
    
    Prüfung "update_user/2 mit gültigen Daten aktualisiert den Benutzer" tun
      
      behaupten Sie Bcrypt.verify_pass ("irgendein aktualisiertes Passwort"user.password_hash)
    Ende

    Prüfung "update_user/2 mit ungültigen Daten gibt Fehler Changeset zurück" tun
      
      %Benutzer{Benutzer | bestätigen Passwort: Null} == Auth.get_user!(user.id) behaupten Bcrypt.verify_pass("irgendein Passwort"user.password_hash)
    Ende
  Ende
Ende

Jetzt mix test sollte keine Fehler liefern.

mix test
..........

Finished in 0.4 seconds
10 tests, 0 failures

Randomized with seed 508666

Generieren Sie einen neuen JSON-Endpunkt

Lassen Sie uns den JSON-Endpunkt des Benutzers generieren, da wir den bereits haben Auth Kontext und User Schema verfügbar,
wir passieren die --no-schema und --no-context Optionen.

mix phx.gen.json Auth User users email:string password:string \
is_active:boolean --no-schema --no-context

Korrigieren Sie die Tests

Wenn Sie nun versuchen, Ihre Tests auszuführen, wird dieser Fehler angezeigt:

== Compilation error in file lib/my_app_web/controllers/user_controller.ex ==
** (CompileError) lib/my_app_web/controllers/user_controller.ex:18: undefined function user_path/3
    (stdlib) lists.erl:1338: :lists.foreach/2
    (stdlib) erl_eval.erl:670: :erl_eval.do_apply/6

Es beschwert sich über ein Vermissen user_path/3 Funktion.

Sie müssen die folgende Zeile hinzufügen lib/my_app_web/router.ex.
Das Deklarieren einer Ressource im Router stellt Controllern einige Helfer zur Verfügung — dh user_path/3:

resources "/users", UserController, except: [:new, :edit]

Dein lib/my_app_web/router.ex sollte so aussehen:

defmodule MyAppWeb.Router do
  
  scope "/api", MyAppWeb do
    pipe_through(:api)
    resources("/users", UserController, except: [:new, :edit])
  end
end

Trotzdem werden Tests noch beschweren.

Um sie zu beheben, müssen wir eine Änderung vornehmen lib/my_app_web/views/user_view.ex,
wir sollten keine senden Passwort Attribut für den Benutzer:

defmodule MyAppWeb.UserView do
  
  def render("user.json", %{user: user}) do
    %{id: user.id, email: user.email, is_active: user.is_active}
  end
  
end

Tests sollten jetzt in Ordnung sein.

Erstellen Sie ein paar Benutzer

Verwenden von IEx

Sie können Ihre App in IEx (Interactive Elixir) ausführen – das ist ähnlich wie rails console— mit:

iex -S mix phx.server

Erstellen Sie dann einen neuen Benutzer mit:

MyApp.Auth.create_user(%{email: "asd@asd.com", password: "qwerty"})

Curl verwenden

Wenn Sie haben curl in Ihrem Terminal verfügbar ist, können Sie einen neuen Benutzer über Ihren Endpunkt erstellen, indem Sie Folgendes verwenden:

curl -H "Content-Type: application/json" -X POST \
-d '{"user":{"email":"some@email.com","password":"some password"}}' \
/api/users

Sie müssen dies konfigurieren, wenn Sie vorhaben, Ihre zu haben API und Frontend auf verschiedenen Domänen.
Wenn Sie nicht wissen, was KOR ist, schau dir das an: Cross-Origin-Ressourcenfreigabe (CORS).

Das heißt, hier haben wir zwei Möglichkeiten:

Ich werde verwenden Korsika in diesem Tutorial, da es mehr Funktionen zum Konfigurieren von CORS enthält
Anfragen — Wenn Sie eine weniger strenge Bibliothek wollen, versuchen Sie es CorsPlug aus.

Fügen Sie diese Abhängigkeit hinzu mix.exs:

  defp deps do
    [
      
      {:corsica, "~> 1.0"}
    ]
  end

Holen Sie sich neue Abhängigkeiten mit:

mix deps.get

Hinzufügen plug Corsica zu lib/my_app_web/endpoint.ex direkt über dem Router-Stecker:

defmodule MyAppWeb.Endpoint do
  
  plug(
    Corsica,
    origins: "http://localhost:8080",
    log: [rejected: :error, invalid: :warn, accepted: :debug],
    allow_headers: ["content-type"],
    allow_credentials: true
  )

  plug(MyAppWeb.Router)
  
end

Sie können eine Liste an übergeben origins aus denen man sich zusammensetzen kann Saiten und/oder regulär
Ausdrücke.

In meinem Fall akzeptiert die obige Regel CORS-Anfragen von a Vue.js Frontend
das nutzt Axios für HTTP-Anforderungen — Vue.js-Entwicklungsserver gehen auf Port
8080 standardmäßig.


Überprüfen Sie das Kennwort eines Benutzers

Lassen Sie uns einige Funktionen zu hinzufügen lib/my_app/auth/auth.ex Datei, um das Passwort eines Benutzers zu überprüfen:

defmodule MyApp.Auth do
  
  def authenticate_user(email, password) do
    query = from(u in User, where: u.email == ^email)
    query |> Repo.one() |> verify_password(password)
  end

  defp verify_password(nil, _) do
    
    Bcrypt.no_user_verify()
    {:error, "Wrong email or password"}
  end

  defp verify_password(user, password) do
    if Bcrypt.verify_pass(password, user.password_hash) do
      {:ok, user}
    else
      {:error, "Wrong email or password"}
    end
  end
end

sign_in-Endpunkt

Dann füge eine neue hinzu sign_in Endpunkt zu lib/my_app_web/router.ex:

defmodule MyAppWeb.Router do
  
  scope "/api", MyAppWeb do
    pipe_through(:api)
    resources("/users", UserController, except: [:new, :edit])
    post("/users/sign_in", UserController, :sign_in)
  end
end

sign_in Controller-Funktion

Fügen Sie abschließend die hinzu sign_in Funktion zu lib/my_app_web/controllers/user_controller.ex:

defmodule MyAppWeb.UserController do
  
  def sign_in(conn, %{"email" => email, "password" => password}) do
    case MyApp.Auth.authenticate_user(email, password) do
      {:ok, user} ->
        conn
        |> put_status(:ok)
        |> render(MyAppWeb.UserView, "sign_in.json", user: user)

      {:error, message} ->
        conn
        |> put_status(:unauthorized)
        |> render(MyAppWeb.ErrorView, "401.json", message: message)
    end
  end
end

Beachten Sie, dass wir Innenansichten rendern MyAppWeb nicht im Inneren MyApp.

Definieren Sie sing_in.json- und 401.json-Ansichten

Im lib/my_app_web/user_view.ex füge das hinzu:

defmodule MyAppWeb.UserView do
  
  def render("sign_in.json", %{user: user}) do
    %{
      data: %{
        user: %{
          id: user.id,
          email: user.email
        }
      }
    }
  end
end

Im lib/my_app_web/error_view.ex füge das hinzu:

defmodule MyAppWeb.ErrorView do
  
  def render("401.json", %{message: message}) do
    %{errors: %{detail: message}}
  end
end

Sie können die versuchen sign_in Endpunkt jetzt.

Probieren Sie Ihren neuen Endpunkt mit curl aus

Lassen Sie uns unseren Entwicklungsserver neu starten und einige POST-Anforderungen an senden /api/users/sign_in.

Gute Zeugnisse

curl -H "Content-Type: application/json" -X POST \
-d '{"email":"asd@asd.com","password":"qwerty"}' \
/api/users/sign_in -i

Sie erhalten eine 200 mit:

{
  "data": {
    "user": { "id": 1,  "email": "asd@asd.com" }
  }
}

schlechte Referenzen

curl -H "Content-Type: application/json" -X POST \
-d '{"email":"asd@asd.com","password":"not the right password"}' \
/api/users/sign_in -i

Du bekommst ein 401 mit:

{ "errors": { "detail": "Wrong email or password" } }

Sitzungen

Hinzufügen plug :fetch_session zu deinem :api Pipeline ein lib/my_app_web/router.ex:

defmodule MyAppWeb.Router do
  
  pipeline :api do
    plug(:accepts, ["json"])
    plug(:fetch_session)
  end
  
end

Authentifizierungsstatus speichern

Jetzt ändern wir unsere sign_in Funktion ein lib/my_app_web/controllers/user_controller.ex:

defmodule MyAppWeb.UserController do
  
  def sign_in(conn, %{"email" => email, "password" => password}) do
    case MyApp.Auth.authenticate_user(email, password) do
      {:ok, user} ->
        conn
        |> put_session(:current_user_id, user.id)
        |> put_status(:ok)
        |> render(MyAppWeb.UserView, "sign_in.json", user: user)

      {:error, message} ->
        conn
        |> delete_session(:current_user_id)
        |> put_status(:unauthorized)
        |> render(MyAppWeb.ErrorView, "401.json", message: message)
    end
  end
end

Schützen Sie eine Ressource mit Authentifizierung

Ändern Sie Ihre lib/my_app_web/router.ex so aussehen:

defmodule MyAppWeb.Router do
  use MyAppWeb, :router

  pipeline :api do
    plug(:accepts, ["json"])
    plug(:fetch_session)
  end

  pipeline :api_auth do
    plug(:ensure_authenticated)
  end

  scope "/api", MyAppWeb do
    pipe_through(:api)
    post("/users/sign_in", UserController, :sign_in)
  end

  scope "/api", MyAppWeb do
    pipe_through([:api, :api_auth])
    resources("/users", UserController, except: [:new, :edit])
  end

  
  defp ensure_authenticated(conn, _opts) do
    current_user_id = get_session(conn, :current_user_id)

    if current_user_id do
      conn
    else
      conn
      |> put_status(:unauthorized)
      |> render(MyAppWeb.ErrorView, "401.json", message: "Unauthenticated user")
      |> halt()
    end
  end
end

Wie Sie sehen können, haben wir eine neue hinzugefügt Pipeline genannt :api_auth das wird Anfragen ausführen
durch ein neues :ensure_authenticated Stecker Funktion.

Wir haben auch eine neue erstellt scope "/api" Block, der seine Anfragen durchleitet :api
dann durch :api_auth und bewegt resources "/users" Innerhalb.


Ist es nicht erstaunlich, wie man dieses Zeug in Phoenix definieren kann?!
Zusammensetzbarkeit FTW!


Korrigieren Sie die Tests

Natürlich alle unsere MyAppWeb.UserController Tests sind jetzt wegen der gebrochen
Anforderung zu sein eingeloggt. Ich lasse hier, wie das behoben wird
test/my_app_web/controllers/user_controller_test.exs Datei sieht so aus:

defmodule MyAppWeb.UserControllerTest do
  use MyAppWeb.ConnCase

  alias MyApp.Auth
  alias MyApp.Auth.User
  alias Plug.Test

  @create_attrs %{email: "some email", is_active: true, password: "some password"}
  @update_attrs %{
    email: "some updated email",
    is_active: false,
    password: "some updated password"
  }
  @invalid_attrs %{email: nil, is_active: nil, password: nil}
  @current_user_attrs %{
    email: "some current user email",
    is_active: true,
    password: "some current user password"
  }

  def fixture(:user) do
    {:ok, user} = Auth.create_user(@create_attrs)
    user
  end

  def fixture(:current_user) do
    {:ok, current_user} = Auth.create_user(@current_user_attrs)
    current_user
  end

  setup %{conn: conn} do
    {:ok, conn: conn, current_user: current_user} = setup_current_user(conn)
    {:ok, conn: put_req_header(conn, "accept", "application/json"), current_user: current_user}
  end

  describe "index" do
    test "lists all users", %{conn: conn, current_user: current_user} do
      conn = get(conn, user_path(conn, :index))

      assert json_response(conn, 200)["data"] == [
               %{
                 "id" => current_user.id,
                 "email" => current_user.email,
                 "is_active" => current_user.is_active
               }
             ]
    end
  end

  describe "create user" do
    test "renders user when data is valid", %{conn: conn} do
      conn = post(conn, user_path(conn, :create), user: @create_attrs)
      assert %{"id" => id} = json_response(conn, 201)["data"]

      conn = get(conn, user_path(conn, :show, id))

      assert json_response(conn, 200)["data"] == %{
               "id" => id,
               "email" => "some email",
               "is_active" => true
             }
    end

    test "renders errors when data is invalid", %{conn: conn} do
      conn = post(conn, user_path(conn, :create), user: @invalid_attrs)
      assert json_response(conn, 422)["errors"] != %{}
    end
  end

  describe "update user" do
    setup [:create_user]

    test "renders user when data is valid", %{conn: conn, user: %User{id: id} = user} do
      conn = put(conn, user_path(conn, :update, user), user: @update_attrs)
      assert %{"id" => ^id} = json_response(conn, 200)["data"]

      conn = get(conn, user_path(conn, :show, id))

      assert json_response(conn, 200)["data"] == %{
               "id" => id,
               "email" => "some updated email",
               "is_active" => false
             }
    end

    test "renders errors when data is invalid", %{conn: conn, user: user} do
      conn = put(conn, user_path(conn, :update, user), user: @invalid_attrs)
      assert json_response(conn, 422)["errors"] != %{}
    end
  end

  describe "delete user" do
    setup [:create_user]

    test "deletes chosen user", %{conn: conn, user: user} do
      conn = delete(conn, user_path(conn, :delete, user))
      assert response(conn, 204)

      assert_error_sent(404, fn ->
        get(conn, user_path(conn, :show, user))
      end)
    end
  end

  defp create_user(_) do
    user = fixture(:user)
    {:ok, user: user}
  end

  defp setup_current_user(conn) do
    current_user = fixture(:current_user)

    {:ok,
     conn: Test.init_test_session(conn, current_user_id: current_user.id),
     current_user: current_user}
  end
end

Fehlende Tests hinzufügen

Im test/my_app/auth/auth_test.exstesten Sie die authenticate_user/2 Funktion:

defmodule MyApp.AuthTest do
  
  describe "users" do
    
    test "authenticate_user/2 authenticates the user" do
      user = user_fixture()
      assert {:error, "Wrong email or password"} = Auth.authenticate_user("wrong email", "")
      assert {:ok, authenticated_user} = Auth.authenticate_user(user.email, @valid_attrs.password)
      assert %User{user | password: nil} == authenticated_user
    end
  end
end

Im test/my_app_web/controllers/user_controller_test.exstesten Sie die sign_in Endpunkt:

defmodule MyAppWeb.UserControllerTest do
  
  describe "sign_in user" do
    test "renders user when user credentials are good", %{conn: conn, current_user: current_user} do
      conn =
        post(
          conn,
          user_path(conn, :sign_in, %{
            email: current_user.email,
            password: @current_user_attrs.password
          })
        )

      assert json_response(conn, 200)["data"] == %{
               "user" => %{"id" => current_user.id, "email" => current_user.email}
             }
    end

    test "renders errors when user credentials are bad", %{conn: conn} do
      conn = post(conn, user_path(conn, :sign_in, %{email: "nonexistent email", password: ""}))
      assert json_response(conn, 401)["errors"] == %{"detail" => "Wrong email or password"}
    end
  end
  
end

Versuchen Sie, eine geschützte Ressource anzufordern, z /api/users mit:

curl -H "Content-Type: application/json" -X GET \
/api/users \
-c cookies.txt -b cookies.txt -i

Du wirst kriegen:

{ "errors": { "detail": "Unauthenticated user" } }

Melden wir uns an mit:

curl -H "Content-Type: application/json" -X POST \
-d '{"email":"asd@asd.com","password":"qwerty"}' \
/api/users/sign_in \
-c cookies.txt -b cookies.txt -i

Du wirst kriegen:

{
  "data": {
    "user": { "id": 1, "email": "asd@asd.com" }
  }
}

Versuchen Sie nun, diese geschützte Ressource erneut anzufordern:

curl -H "Content-Type: application/json" -X GET \
/api/users \
-c cookies.txt -b cookies.txt -i

Du wirst sehen:

{
  "data": [
    { "is_active": false, "id": 1, "email": "asd@asd.com" },
    { "is_active": false, "id": 2, "email": "some@email.com" }
  ]
}

Erfolg!


Passen Sie Ihre 404- und 500-JSON-Antworten an

Im lib/my_app_web/views/error_view.ex:

defmodule MyAppWeb.ErrorView do
  
  def render("404.json", _assigns) do
    %{errors: %{detail: "Endpoint not found!"}}
  end

  def render("500.json", _assigns) do
    %{errors: %{detail: "Internal server error :("}}
  end
  
end

Formatieren Sie die Dateien Ihres Projekts mit dem integrierten Code-Formatierer von Elixir

Erstelle eine neue .formatter.exs Datei im Stammverzeichnis Ihres Projekts mit diesem Inhalt:

[
  inputs: ["mix.exs", "{config,lib,priv,test}/**/*.{ex,exs}"]
]

Rufen Sie jetzt auf mix format um Ihr gesamtes Projekt gemäß der Vorgabe zu formatieren Elixier Formatierungsregeln.

Geben Sie den Port des Phoenix-Servers an

Standardmäßig ausgeführt mix phx.server wird Ihre Anwendung im Hafen bedienen 4000machen wir den Port konfigurierbar für
unser Entwicklung Umgebung. Im config/dev.exs Ändern Sie die Zeile für http: [port: 4000], zu:

config :my_app, MyAppWeb.Endpoint,
  http: [port: System.get_env("PORT") || 4000],
  

Nun, um Ihre App beim Starten an einen anderen Port zu binden — sagen wir mal 5000— tun:

PORT=5000 mix phx.server

PORT=5000 iex -S mix phx.server

Visual Studio Code-Erweiterung für Elixir

Ich empfehle mit zu gehen ElixierLS, Es ist ziemlich aktuell und hat viele erweiterte Funktionen, schau es dir an hier.

Übungen für den Leser

Hier sind einige Aufgaben, an denen Sie sich versuchen könnten:

  • Berücksichtigen Sie die eines Benutzers is_active -Attribut, wenn Sie versuchen, sich anzumelden.
  • Implementieren Sie ein /api/users/sign_out Endpunkt an UserController.
  • Mach es SICH AUSRUHENy: Extrahieren sign_in und sign_out‘s Funktionalität von
    UserController auf ihren eigenen Controller.
    Vielleicht anrufen SessionControllerwo UserController.sign_in sollte sein
    SessionController.create und UserController.sign_out sollte sein SessionController.delete.
  • Implementieren DB Unterstützung für Sitzungen.
  • Implementieren Sie ein /api/me Endpunkt auf einem neuen MeController.
    Dies kann als dienen Klingeln Endpunkt, um zu überprüfen, ob der Benutzer noch angemeldet ist. Sollte zurückkehren
    das current_user Information.
  • Passen Sie Tests für all diese neuen Funktionen an und erstellen Sie Tests.

i18n

Über Locken

Lernen


Das war lang, das war’s für jetzt, Leute!

— lt

Similar Posts

Leave a Reply

Your email address will not be published.