diff --git a/Elixir/notes_app/.formatter.exs b/Elixir/notes_app/.formatter.exs
new file mode 100644
index 0000000..ef8840c
--- /dev/null
+++ b/Elixir/notes_app/.formatter.exs
@@ -0,0 +1,6 @@
+[
+ import_deps: [:ecto, :ecto_sql, :phoenix],
+ subdirectories: ["priv/*/migrations"],
+ plugins: [Phoenix.LiveView.HTMLFormatter],
+ inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"]
+]
diff --git a/Elixir/notes_app/.gitignore b/Elixir/notes_app/.gitignore
new file mode 100644
index 0000000..de5830c
--- /dev/null
+++ b/Elixir/notes_app/.gitignore
@@ -0,0 +1,41 @@
+# The directory Mix will write compiled artifacts to.
+/_build/
+
+# If you run "mix test --cover", coverage assets end up here.
+/cover/
+
+# The directory Mix downloads your dependencies sources to.
+/deps/
+
+# Where 3rd-party dependencies like ExDoc output generated docs.
+/doc/
+
+# Ignore .fetch files in case you like to edit your project deps locally.
+/.fetch
+
+# If the VM crashes, it generates a dump, let's ignore it too.
+erl_crash.dump
+
+# Also ignore archive artifacts (built via "mix archive.build").
+*.ez
+
+# Temporary files, for example, from tests.
+/tmp/
+
+# Ignore package tarball (built via "mix hex.build").
+notes_app-*.tar
+
+# Ignore assets that are produced by build tools.
+/priv/static/assets/
+
+# Ignore digested assets cache.
+/priv/static/cache_manifest.json
+
+# In case you use Node.js/npm, you want to ignore these.
+npm-debug.log
+/assets/node_modules/
+
+# Database files
+*.db
+*.db-*
+
diff --git a/Elixir/notes_app/README.md b/Elixir/notes_app/README.md
new file mode 100644
index 0000000..bb65b4b
--- /dev/null
+++ b/Elixir/notes_app/README.md
@@ -0,0 +1,18 @@
+# NotesApp
+
+To start your Phoenix server:
+
+ * Run `mix setup` to install and setup dependencies
+ * Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server`
+
+Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
+
+Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html).
+
+## Learn more
+
+ * Official website: https://www.phoenixframework.org/
+ * Guides: https://hexdocs.pm/phoenix/overview.html
+ * Docs: https://hexdocs.pm/phoenix
+ * Forum: https://elixirforum.com/c/phoenix-forum
+ * Source: https://github.com/phoenixframework/phoenix
diff --git a/Elixir/notes_app/assets/css/app.css b/Elixir/notes_app/assets/css/app.css
new file mode 100644
index 0000000..378c8f9
--- /dev/null
+++ b/Elixir/notes_app/assets/css/app.css
@@ -0,0 +1,5 @@
+@import "tailwindcss/base";
+@import "tailwindcss/components";
+@import "tailwindcss/utilities";
+
+/* This file is for your main application CSS */
diff --git a/Elixir/notes_app/assets/js/app.js b/Elixir/notes_app/assets/js/app.js
new file mode 100644
index 0000000..d5e278a
--- /dev/null
+++ b/Elixir/notes_app/assets/js/app.js
@@ -0,0 +1,44 @@
+// If you want to use Phoenix channels, run `mix help phx.gen.channel`
+// to get started and then uncomment the line below.
+// import "./user_socket.js"
+
+// You can include dependencies in two ways.
+//
+// The simplest option is to put them in assets/vendor and
+// import them using relative paths:
+//
+// import "../vendor/some-package.js"
+//
+// Alternatively, you can `npm install some-package --prefix assets` and import
+// them using a path starting with the package name:
+//
+// import "some-package"
+//
+
+// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
+import "phoenix_html"
+// Establish Phoenix Socket and LiveView configuration.
+import {Socket} from "phoenix"
+import {LiveSocket} from "phoenix_live_view"
+import topbar from "../vendor/topbar"
+
+let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
+let liveSocket = new LiveSocket("/live", Socket, {
+ longPollFallbackMs: 2500,
+ params: {_csrf_token: csrfToken}
+})
+
+// Show progress bar on live navigation and form submits
+topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
+window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
+window.addEventListener("phx:page-loading-stop", _info => topbar.hide())
+
+// connect if there are any LiveViews on the page
+liveSocket.connect()
+
+// expose liveSocket on window for web console debug logs and latency simulation:
+// >> liveSocket.enableDebug()
+// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
+// >> liveSocket.disableLatencySim()
+window.liveSocket = liveSocket
+
diff --git a/Elixir/notes_app/assets/tailwind.config.js b/Elixir/notes_app/assets/tailwind.config.js
new file mode 100644
index 0000000..9a8d923
--- /dev/null
+++ b/Elixir/notes_app/assets/tailwind.config.js
@@ -0,0 +1,74 @@
+// See the Tailwind configuration guide for advanced usage
+// https://tailwindcss.com/docs/configuration
+
+const plugin = require("tailwindcss/plugin")
+const fs = require("fs")
+const path = require("path")
+
+module.exports = {
+ content: [
+ "./js/**/*.js",
+ "../lib/notes_app_web.ex",
+ "../lib/notes_app_web/**/*.*ex"
+ ],
+ theme: {
+ extend: {
+ colors: {
+ brand: "#FD4F00",
+ }
+ },
+ },
+ plugins: [
+ require("@tailwindcss/forms"),
+ // Allows prefixing tailwind classes with LiveView classes to add rules
+ // only when LiveView classes are applied, for example:
+ //
+ //
+ //
+ plugin(({addVariant}) => addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"])),
+ plugin(({addVariant}) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])),
+ plugin(({addVariant}) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"])),
+
+ // Embeds Heroicons (https://heroicons.com) into your app.css bundle
+ // See your `CoreComponents.icon/1` for more information.
+ //
+ plugin(function({matchComponents, theme}) {
+ let iconsDir = path.join(__dirname, "../deps/heroicons/optimized")
+ let values = {}
+ let icons = [
+ ["", "/24/outline"],
+ ["-solid", "/24/solid"],
+ ["-mini", "/20/solid"],
+ ["-micro", "/16/solid"]
+ ]
+ icons.forEach(([suffix, dir]) => {
+ fs.readdirSync(path.join(iconsDir, dir)).forEach(file => {
+ let name = path.basename(file, ".svg") + suffix
+ values[name] = {name, fullPath: path.join(iconsDir, dir, file)}
+ })
+ })
+ matchComponents({
+ "hero": ({name, fullPath}) => {
+ let content = fs.readFileSync(fullPath).toString().replace(/\r?\n|\r/g, "")
+ let size = theme("spacing.6")
+ if (name.endsWith("-mini")) {
+ size = theme("spacing.5")
+ } else if (name.endsWith("-micro")) {
+ size = theme("spacing.4")
+ }
+ return {
+ [`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`,
+ "-webkit-mask": `var(--hero-${name})`,
+ "mask": `var(--hero-${name})`,
+ "mask-repeat": "no-repeat",
+ "background-color": "currentColor",
+ "vertical-align": "middle",
+ "display": "inline-block",
+ "width": size,
+ "height": size
+ }
+ }
+ }, {values})
+ })
+ ]
+}
diff --git a/Elixir/notes_app/assets/vendor/topbar.js b/Elixir/notes_app/assets/vendor/topbar.js
new file mode 100644
index 0000000..4195727
--- /dev/null
+++ b/Elixir/notes_app/assets/vendor/topbar.js
@@ -0,0 +1,165 @@
+/**
+ * @license MIT
+ * topbar 2.0.0, 2023-02-04
+ * https://buunguyen.github.io/topbar
+ * Copyright (c) 2021 Buu Nguyen
+ */
+(function (window, document) {
+ "use strict";
+
+ // https://gist.github.com/paulirish/1579671
+ (function () {
+ var lastTime = 0;
+ var vendors = ["ms", "moz", "webkit", "o"];
+ for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
+ window.requestAnimationFrame =
+ window[vendors[x] + "RequestAnimationFrame"];
+ window.cancelAnimationFrame =
+ window[vendors[x] + "CancelAnimationFrame"] ||
+ window[vendors[x] + "CancelRequestAnimationFrame"];
+ }
+ if (!window.requestAnimationFrame)
+ window.requestAnimationFrame = function (callback, element) {
+ var currTime = new Date().getTime();
+ var timeToCall = Math.max(0, 16 - (currTime - lastTime));
+ var id = window.setTimeout(function () {
+ callback(currTime + timeToCall);
+ }, timeToCall);
+ lastTime = currTime + timeToCall;
+ return id;
+ };
+ if (!window.cancelAnimationFrame)
+ window.cancelAnimationFrame = function (id) {
+ clearTimeout(id);
+ };
+ })();
+
+ var canvas,
+ currentProgress,
+ showing,
+ progressTimerId = null,
+ fadeTimerId = null,
+ delayTimerId = null,
+ addEvent = function (elem, type, handler) {
+ if (elem.addEventListener) elem.addEventListener(type, handler, false);
+ else if (elem.attachEvent) elem.attachEvent("on" + type, handler);
+ else elem["on" + type] = handler;
+ },
+ options = {
+ autoRun: true,
+ barThickness: 3,
+ barColors: {
+ 0: "rgba(26, 188, 156, .9)",
+ ".25": "rgba(52, 152, 219, .9)",
+ ".50": "rgba(241, 196, 15, .9)",
+ ".75": "rgba(230, 126, 34, .9)",
+ "1.0": "rgba(211, 84, 0, .9)",
+ },
+ shadowBlur: 10,
+ shadowColor: "rgba(0, 0, 0, .6)",
+ className: null,
+ },
+ repaint = function () {
+ canvas.width = window.innerWidth;
+ canvas.height = options.barThickness * 5; // need space for shadow
+
+ var ctx = canvas.getContext("2d");
+ ctx.shadowBlur = options.shadowBlur;
+ ctx.shadowColor = options.shadowColor;
+
+ var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
+ for (var stop in options.barColors)
+ lineGradient.addColorStop(stop, options.barColors[stop]);
+ ctx.lineWidth = options.barThickness;
+ ctx.beginPath();
+ ctx.moveTo(0, options.barThickness / 2);
+ ctx.lineTo(
+ Math.ceil(currentProgress * canvas.width),
+ options.barThickness / 2
+ );
+ ctx.strokeStyle = lineGradient;
+ ctx.stroke();
+ },
+ createCanvas = function () {
+ canvas = document.createElement("canvas");
+ var style = canvas.style;
+ style.position = "fixed";
+ style.top = style.left = style.right = style.margin = style.padding = 0;
+ style.zIndex = 100001;
+ style.display = "none";
+ if (options.className) canvas.classList.add(options.className);
+ document.body.appendChild(canvas);
+ addEvent(window, "resize", repaint);
+ },
+ topbar = {
+ config: function (opts) {
+ for (var key in opts)
+ if (options.hasOwnProperty(key)) options[key] = opts[key];
+ },
+ show: function (delay) {
+ if (showing) return;
+ if (delay) {
+ if (delayTimerId) return;
+ delayTimerId = setTimeout(() => topbar.show(), delay);
+ } else {
+ showing = true;
+ if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId);
+ if (!canvas) createCanvas();
+ canvas.style.opacity = 1;
+ canvas.style.display = "block";
+ topbar.progress(0);
+ if (options.autoRun) {
+ (function loop() {
+ progressTimerId = window.requestAnimationFrame(loop);
+ topbar.progress(
+ "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2)
+ );
+ })();
+ }
+ }
+ },
+ progress: function (to) {
+ if (typeof to === "undefined") return currentProgress;
+ if (typeof to === "string") {
+ to =
+ (to.indexOf("+") >= 0 || to.indexOf("-") >= 0
+ ? currentProgress
+ : 0) + parseFloat(to);
+ }
+ currentProgress = to > 1 ? 1 : to;
+ repaint();
+ return currentProgress;
+ },
+ hide: function () {
+ clearTimeout(delayTimerId);
+ delayTimerId = null;
+ if (!showing) return;
+ showing = false;
+ if (progressTimerId != null) {
+ window.cancelAnimationFrame(progressTimerId);
+ progressTimerId = null;
+ }
+ (function loop() {
+ if (topbar.progress("+.1") >= 1) {
+ canvas.style.opacity -= 0.05;
+ if (canvas.style.opacity <= 0.05) {
+ canvas.style.display = "none";
+ fadeTimerId = null;
+ return;
+ }
+ }
+ fadeTimerId = window.requestAnimationFrame(loop);
+ })();
+ },
+ };
+
+ if (typeof module === "object" && typeof module.exports === "object") {
+ module.exports = topbar;
+ } else if (typeof define === "function" && define.amd) {
+ define(function () {
+ return topbar;
+ });
+ } else {
+ this.topbar = topbar;
+ }
+}.call(this, window, document));
diff --git a/Elixir/notes_app/config/config.exs b/Elixir/notes_app/config/config.exs
new file mode 100644
index 0000000..a603431
--- /dev/null
+++ b/Elixir/notes_app/config/config.exs
@@ -0,0 +1,66 @@
+# This file is responsible for configuring your application
+# and its dependencies with the aid of the Config module.
+#
+# This configuration file is loaded before any dependency and
+# is restricted to this project.
+
+# General application configuration
+import Config
+
+config :notes_app,
+ ecto_repos: [NotesApp.Repo],
+ generators: [timestamp_type: :utc_datetime]
+
+# Configures the endpoint
+config :notes_app, NotesAppWeb.Endpoint,
+ url: [host: "localhost"],
+ adapter: Bandit.PhoenixAdapter,
+ render_errors: [
+ formats: [html: NotesAppWeb.ErrorHTML, json: NotesAppWeb.ErrorJSON],
+ layout: false
+ ],
+ pubsub_server: NotesApp.PubSub,
+ live_view: [signing_salt: "He9WNGLS"]
+
+# Configures the mailer
+#
+# By default it uses the "Local" adapter which stores the emails
+# locally. You can see the emails in your browser, at "/dev/mailbox".
+#
+# For production it's recommended to configure a different adapter
+# at the `config/runtime.exs`.
+config :notes_app, NotesApp.Mailer, adapter: Swoosh.Adapters.Local
+
+# Configure esbuild (the version is required)
+config :esbuild,
+ version: "0.17.11",
+ notes_app: [
+ args:
+ ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
+ cd: Path.expand("../assets", __DIR__),
+ env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
+ ]
+
+# Configure tailwind (the version is required)
+config :tailwind,
+ version: "3.4.3",
+ notes_app: [
+ args: ~w(
+ --config=tailwind.config.js
+ --input=css/app.css
+ --output=../priv/static/assets/app.css
+ ),
+ cd: Path.expand("../assets", __DIR__)
+ ]
+
+# Configures Elixir's Logger
+config :logger, :console,
+ format: "$time $metadata[$level] $message\n",
+ metadata: [:request_id]
+
+# Use Jason for JSON parsing in Phoenix
+config :phoenix, :json_library, Jason
+
+# Import environment specific config. This must remain at the bottom
+# of this file so it overrides the configuration defined above.
+import_config "#{config_env()}.exs"
diff --git a/Elixir/notes_app/config/dev.exs b/Elixir/notes_app/config/dev.exs
new file mode 100644
index 0000000..1769fac
--- /dev/null
+++ b/Elixir/notes_app/config/dev.exs
@@ -0,0 +1,82 @@
+import Config
+
+# Configure your database
+config :notes_app, NotesApp.Repo,
+ database: Path.expand("../notes_app_dev.db", __DIR__),
+ pool_size: 5,
+ stacktrace: true,
+ show_sensitive_data_on_connection_error: true
+
+# For development, we disable any cache and enable
+# debugging and code reloading.
+#
+# The watchers configuration can be used to run external
+# watchers to your application. For example, we can use it
+# to bundle .js and .css sources.
+config :notes_app, NotesAppWeb.Endpoint,
+ # Binding to loopback ipv4 address prevents access from other machines.
+ # Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
+ http: [ip: {127, 0, 0, 1}, port: 4000],
+ check_origin: false,
+ code_reloader: true,
+ debug_errors: true,
+ secret_key_base: "mQh9OiutrVG3ttXYKwX0owXUInJ+SI16fMmOmYzJ+/e9/zq5wgPFRAPDCncLs9sC",
+ watchers: [
+ esbuild: {Esbuild, :install_and_run, [:notes_app, ~w(--sourcemap=inline --watch)]},
+ tailwind: {Tailwind, :install_and_run, [:notes_app, ~w(--watch)]}
+ ]
+
+# ## SSL Support
+#
+# In order to use HTTPS in development, a self-signed
+# certificate can be generated by running the following
+# Mix task:
+#
+# mix phx.gen.cert
+#
+# Run `mix help phx.gen.cert` for more information.
+#
+# The `http:` config above can be replaced with:
+#
+# https: [
+# port: 4001,
+# cipher_suite: :strong,
+# keyfile: "priv/cert/selfsigned_key.pem",
+# certfile: "priv/cert/selfsigned.pem"
+# ],
+#
+# If desired, both `http:` and `https:` keys can be
+# configured to run both http and https servers on
+# different ports.
+
+# Watch static and templates for browser reloading.
+config :notes_app, NotesAppWeb.Endpoint,
+ live_reload: [
+ patterns: [
+ ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$",
+ ~r"priv/gettext/.*(po)$",
+ ~r"lib/notes_app_web/(controllers|live|components)/.*(ex|heex)$"
+ ]
+ ]
+
+# Enable dev routes for dashboard and mailbox
+config :notes_app, dev_routes: true
+
+# Do not include metadata nor timestamps in development logs
+config :logger, :console, format: "[$level] $message\n"
+
+# Set a higher stacktrace during development. Avoid configuring such
+# in production as building large stacktraces may be expensive.
+config :phoenix, :stacktrace_depth, 20
+
+# Initialize plugs at runtime for faster development compilation
+config :phoenix, :plug_init_mode, :runtime
+
+config :phoenix_live_view,
+ # Include HEEx debug annotations as HTML comments in rendered markup
+ debug_heex_annotations: true,
+ # Enable helpful, but potentially expensive runtime checks
+ enable_expensive_runtime_checks: true
+
+# Disable swoosh api client as it is only required for production adapters.
+config :swoosh, :api_client, false
diff --git a/Elixir/notes_app/config/prod.exs b/Elixir/notes_app/config/prod.exs
new file mode 100644
index 0000000..b551836
--- /dev/null
+++ b/Elixir/notes_app/config/prod.exs
@@ -0,0 +1,20 @@
+import Config
+
+# Note we also include the path to a cache manifest
+# containing the digested version of static files. This
+# manifest is generated by the `mix assets.deploy` task,
+# which you should run after static files are built and
+# before starting your production server.
+config :notes_app, NotesAppWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json"
+
+# Configures Swoosh API Client
+config :swoosh, api_client: Swoosh.ApiClient.Finch, finch_name: NotesApp.Finch
+
+# Disable Swoosh Local Memory Storage
+config :swoosh, local: false
+
+# Do not print debug messages in production
+config :logger, level: :info
+
+# Runtime production configuration, including reading
+# of environment variables, is done on config/runtime.exs.
diff --git a/Elixir/notes_app/config/runtime.exs b/Elixir/notes_app/config/runtime.exs
new file mode 100644
index 0000000..01fcbdc
--- /dev/null
+++ b/Elixir/notes_app/config/runtime.exs
@@ -0,0 +1,113 @@
+import Config
+
+# config/runtime.exs is executed for all environments, including
+# during releases. It is executed after compilation and before the
+# system starts, so it is typically used to load production configuration
+# and secrets from environment variables or elsewhere. Do not define
+# any compile-time configuration in here, as it won't be applied.
+# The block below contains prod specific runtime configuration.
+
+# ## Using releases
+#
+# If you use `mix release`, you need to explicitly enable the server
+# by passing the PHX_SERVER=true when you start it:
+#
+# PHX_SERVER=true bin/notes_app start
+#
+# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server`
+# script that automatically sets the env var above.
+if System.get_env("PHX_SERVER") do
+ config :notes_app, NotesAppWeb.Endpoint, server: true
+end
+
+if config_env() == :prod do
+ database_path =
+ System.get_env("DATABASE_PATH") ||
+ raise """
+ environment variable DATABASE_PATH is missing.
+ For example: /etc/notes_app/notes_app.db
+ """
+
+ config :notes_app, NotesApp.Repo,
+ database: database_path,
+ pool_size: String.to_integer(System.get_env("POOL_SIZE") || "5")
+
+ # The secret key base is used to sign/encrypt cookies and other secrets.
+ # A default value is used in config/dev.exs and config/test.exs but you
+ # want to use a different value for prod and you most likely don't want
+ # to check this value into version control, so we use an environment
+ # variable instead.
+ secret_key_base =
+ System.get_env("SECRET_KEY_BASE") ||
+ raise """
+ environment variable SECRET_KEY_BASE is missing.
+ You can generate one by calling: mix phx.gen.secret
+ """
+
+ host = System.get_env("PHX_HOST") || "example.com"
+ port = String.to_integer(System.get_env("PORT") || "4000")
+
+ config :notes_app, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
+
+ config :notes_app, NotesAppWeb.Endpoint,
+ url: [host: host, port: 443, scheme: "https"],
+ http: [
+ # Enable IPv6 and bind on all interfaces.
+ # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
+ # See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0
+ # for details about using IPv6 vs IPv4 and loopback vs public addresses.
+ ip: {0, 0, 0, 0, 0, 0, 0, 0},
+ port: port
+ ],
+ secret_key_base: secret_key_base
+
+ # ## SSL Support
+ #
+ # To get SSL working, you will need to add the `https` key
+ # to your endpoint configuration:
+ #
+ # config :notes_app, NotesAppWeb.Endpoint,
+ # https: [
+ # ...,
+ # port: 443,
+ # cipher_suite: :strong,
+ # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
+ # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
+ # ]
+ #
+ # The `cipher_suite` is set to `:strong` to support only the
+ # latest and more secure SSL ciphers. This means old browsers
+ # and clients may not be supported. You can set it to
+ # `:compatible` for wider support.
+ #
+ # `:keyfile` and `:certfile` expect an absolute path to the key
+ # and cert in disk or a relative path inside priv, for example
+ # "priv/ssl/server.key". For all supported SSL configuration
+ # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
+ #
+ # We also recommend setting `force_ssl` in your config/prod.exs,
+ # ensuring no data is ever sent via http, always redirecting to https:
+ #
+ # config :notes_app, NotesAppWeb.Endpoint,
+ # force_ssl: [hsts: true]
+ #
+ # Check `Plug.SSL` for all available options in `force_ssl`.
+
+ # ## Configuring the mailer
+ #
+ # In production you need to configure the mailer to use a different adapter.
+ # Also, you may need to configure the Swoosh API client of your choice if you
+ # are not using SMTP. Here is an example of the configuration:
+ #
+ # config :notes_app, NotesApp.Mailer,
+ # adapter: Swoosh.Adapters.Mailgun,
+ # api_key: System.get_env("MAILGUN_API_KEY"),
+ # domain: System.get_env("MAILGUN_DOMAIN")
+ #
+ # For this example you need include a HTTP client required by Swoosh API client.
+ # Swoosh supports Hackney and Finch out of the box:
+ #
+ # config :swoosh, :api_client, Swoosh.ApiClient.Hackney
+ #
+ # See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details.
+end
diff --git a/Elixir/notes_app/config/test.exs b/Elixir/notes_app/config/test.exs
new file mode 100644
index 0000000..805a4bd
--- /dev/null
+++ b/Elixir/notes_app/config/test.exs
@@ -0,0 +1,37 @@
+import Config
+
+# Only in tests, remove the complexity from the password hashing algorithm
+config :bcrypt_elixir, :log_rounds, 1
+
+# Configure your database
+#
+# The MIX_TEST_PARTITION environment variable can be used
+# to provide built-in test partitioning in CI environment.
+# Run `mix help test` for more information.
+config :notes_app, NotesApp.Repo,
+ database: Path.expand("../notes_app_test.db", __DIR__),
+ pool_size: 5,
+ pool: Ecto.Adapters.SQL.Sandbox
+
+# We don't run a server during test. If one is required,
+# you can enable the server option below.
+config :notes_app, NotesAppWeb.Endpoint,
+ http: [ip: {127, 0, 0, 1}, port: 4002],
+ secret_key_base: "YKtYPCCPpdbg9Lh7QLm0wHREqg/j4LCe/cd1TrngAaLMSwl/pN+eVN/TYLl0nzk1",
+ server: false
+
+# In test we don't send emails
+config :notes_app, NotesApp.Mailer, adapter: Swoosh.Adapters.Test
+
+# Disable swoosh api client as it is only required for production adapters
+config :swoosh, :api_client, false
+
+# Print only warnings and errors during test
+config :logger, level: :warning
+
+# Initialize plugs at runtime for faster test compilation
+config :phoenix, :plug_init_mode, :runtime
+
+# Enable helpful, but potentially expensive runtime checks
+config :phoenix_live_view,
+ enable_expensive_runtime_checks: true
diff --git a/Elixir/notes_app/mix.exs b/Elixir/notes_app/mix.exs
new file mode 100644
index 0000000..461e174
--- /dev/null
+++ b/Elixir/notes_app/mix.exs
@@ -0,0 +1,87 @@
+defmodule NotesApp.MixProject do
+ use Mix.Project
+
+ def project do
+ [
+ app: :notes_app,
+ version: "0.1.0",
+ elixir: "~> 1.14",
+ elixirc_paths: elixirc_paths(Mix.env()),
+ start_permanent: Mix.env() == :prod,
+ aliases: aliases(),
+ deps: deps()
+ ]
+ end
+
+ # Configuration for the OTP application.
+ #
+ # Type `mix help compile.app` for more information.
+ def application do
+ [
+ mod: {NotesApp.Application, []},
+ extra_applications: [:logger, :runtime_tools]
+ ]
+ end
+
+ # Specifies which paths to compile per environment.
+ defp elixirc_paths(:test), do: ["lib", "test/support"]
+ defp elixirc_paths(_), do: ["lib"]
+
+ # Specifies your project dependencies.
+ #
+ # Type `mix help deps` for examples and options.
+ defp deps do
+ [
+ {:bcrypt_elixir, "~> 3.0"},
+ {:phoenix, "~> 1.7.14"},
+ {:phoenix_ecto, "~> 4.5"},
+ {:ecto_sql, "~> 3.10"},
+ {:ecto_sqlite3, ">= 0.0.0"},
+ {:phoenix_html, "~> 4.1"},
+ {:phoenix_live_reload, "~> 1.2", only: :dev},
+ # TODO bump on release to {:phoenix_live_view, "~> 1.0.0"},
+ {:phoenix_live_view, "~> 1.0.0-rc.1", override: true},
+ {:floki, ">= 0.30.0", only: :test},
+ {:phoenix_live_dashboard, "~> 0.8.3"},
+ {:esbuild, "~> 0.8", runtime: Mix.env() == :dev},
+ {:tailwind, "~> 0.2", runtime: Mix.env() == :dev},
+ {:heroicons,
+ github: "tailwindlabs/heroicons",
+ tag: "v2.1.1",
+ sparse: "optimized",
+ app: false,
+ compile: false,
+ depth: 1},
+ {:swoosh, "~> 1.5"},
+ {:finch, "~> 0.13"},
+ {:telemetry_metrics, "~> 1.0"},
+ {:telemetry_poller, "~> 1.0"},
+ {:gettext, "~> 0.20"},
+ {:jason, "~> 1.2"},
+ {:dns_cluster, "~> 0.1.1"},
+ {:bandit, "~> 1.5"}
+ ]
+ end
+
+ # Aliases are shortcuts or tasks specific to the current project.
+ # For example, to install project dependencies and perform other setup tasks, run:
+ #
+ # $ mix setup
+ #
+ # See the documentation for `Mix` for more info on aliases.
+ defp aliases do
+ [
+ setup: ["deps.get", "ecto.setup", "assets.setup", "assets.build"],
+ "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
+ "ecto.reset": ["ecto.drop", "ecto.setup"],
+ test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
+ "assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"],
+ "assets.build": ["tailwind notes_app", "esbuild notes_app"],
+ "assets.deploy": [
+ "tailwind notes_app --minify",
+ "esbuild notes_app --minify",
+ "phx.digest"
+ ]
+ ]
+ end
+end
diff --git a/Elixir/notes_app/mix.lock b/Elixir/notes_app/mix.lock
new file mode 100644
index 0000000..8aa3628
--- /dev/null
+++ b/Elixir/notes_app/mix.lock
@@ -0,0 +1,46 @@
+%{
+ "bandit": {:hex, :bandit, "1.5.7", "6856b1e1df4f2b0cb3df1377eab7891bec2da6a7fd69dc78594ad3e152363a50", [:mix], [{:hpax, "~> 1.0.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "f2dd92ae87d2cbea2fa9aa1652db157b6cba6c405cb44d4f6dd87abba41371cd"},
+ "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.1.0", "0b110a9a6c619b19a7f73fa3004aa11d6e719a67e672d1633dc36b6b2290a0f7", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ad2acb5a8bc049e8d5aa267802631912bb80d5f4110a178ae7999e69dca1bf7"},
+ "castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"},
+ "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"},
+ "comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"},
+ "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
+ "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
+ "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"},
+ "ecto": {:hex, :ecto, "3.12.2", "bae2094f038e9664ce5f089e5f3b6132a535d8b018bd280a485c2f33df5c0ce1", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "492e67c70f3a71c6afe80d946d3ced52ecc57c53c9829791bfff1830ff5a1f0c"},
+ "ecto_sql": {:hex, :ecto_sql, "3.12.0", "73cea17edfa54bde76ee8561b30d29ea08f630959685006d9c6e7d1e59113b7d", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dc9e4d206f274f3947e96142a8fdc5f69a2a6a9abb4649ef5c882323b6d512f0"},
+ "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.17.1", "96d08639aa1fb07a087daa2220ebc68d9f4f5dbb8aed930e771d848d8d472d32", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "dbd6a68b5364fe6e9ce5d70336709d62edaebbb4cb4acb5f13ec825f64fa48cc"},
+ "elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"},
+ "esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"},
+ "expo": {:hex, :expo, "1.0.1", "f9e2f984f5b8d195815d52d0ba264798c12c8d2f2606f76fa4c60e8ebe39474d", [:mix], [], "hexpm", "f250b33274e3e56513644858c116f255d35c767c2b8e96a512fe7839ef9306a1"},
+ "exqlite": {:hex, :exqlite, "0.23.0", "6e851c937a033299d0784994c66da24845415072adbc455a337e20087bce9033", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "404341cceec5e6466aaed160cf0b58be2019b60af82588c215e1224ebd3ec831"},
+ "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"},
+ "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"},
+ "floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"},
+ "gettext": {:hex, :gettext, "0.26.1", "38e14ea5dcf962d1fc9f361b63ea07c0ce715a8ef1f9e82d3dfb8e67e0416715", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "01ce56f188b9dc28780a52783d6529ad2bc7124f9744e571e1ee4ea88bf08734"},
+ "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]},
+ "hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"},
+ "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
+ "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
+ "mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"},
+ "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
+ "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
+ "phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"},
+ "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.2", "3b83b24ab5a2eb071a20372f740d7118767c272db386831b2e77638c4dcc606d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "3f94d025f59de86be00f5f8c5dd7b5965a3298458d21ab1c328488be3b5fcd59"},
+ "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"},
+ "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.4", "4508e481f791ce62ec6a096e13b061387158cbeefacca68c6c1928e1305e23ed", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "2984aae96994fbc5c61795a73b8fb58153b41ff934019cfb522343d2d3817d59"},
+ "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"},
+ "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.0-rc.6", "47d2669995ea326e5c71f5c1bc9177109cebf211385c638faa7b5862a401e516", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e56e4f1642a0b20edc2488cab30e5439595e0d8b5b259f76ef98b1c4e2e5b527"},
+ "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
+ "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
+ "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
+ "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
+ "swoosh": {:hex, :swoosh, "1.16.12", "cbb24ad512f2f7f24c7a469661c188a00a8c2cd64e0ab54acd1520f132092dfd", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0e262df1ae510d59eeaaa3db42189a2aa1b3746f73771eb2616fc3f7ee63cc20"},
+ "tailwind": {:hex, :tailwind, "0.2.3", "277f08145d407de49650d0a4685dc062174bdd1ae7731c5f1da86163a24dfcdb", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "8e45e7a34a676a7747d04f7913a96c770c85e6be810a1d7f91e713d3a3655b5d"},
+ "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
+ "telemetry_metrics": {:hex, :telemetry_metrics, "1.0.0", "29f5f84991ca98b8eb02fc208b2e6de7c95f8bb2294ef244a176675adc7775df", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f23713b3847286a534e005126d4c959ebcca68ae9582118ce436b521d1d47d5d"},
+ "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"},
+ "thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"},
+ "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
+ "websock_adapter": {:hex, :websock_adapter, "0.5.7", "65fa74042530064ef0570b75b43f5c49bb8b235d6515671b3d250022cb8a1f9e", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d0f478ee64deddfec64b800673fd6e0c8888b079d9f3444dd96d2a98383bdbd1"},
+}
diff --git a/Elixir/notes_app/priv/gettext/en/LC_MESSAGES/errors.po b/Elixir/notes_app/priv/gettext/en/LC_MESSAGES/errors.po
new file mode 100644
index 0000000..844c4f5
--- /dev/null
+++ b/Elixir/notes_app/priv/gettext/en/LC_MESSAGES/errors.po
@@ -0,0 +1,112 @@
+## `msgid`s in this file come from POT (.pot) files.
+##
+## Do not add, change, or remove `msgid`s manually here as
+## they're tied to the ones in the corresponding POT file
+## (with the same domain).
+##
+## Use `mix gettext.extract --merge` or `mix gettext.merge`
+## to merge POT files into PO files.
+msgid ""
+msgstr ""
+"Language: en\n"
+
+## From Ecto.Changeset.cast/4
+msgid "can't be blank"
+msgstr ""
+
+## From Ecto.Changeset.unique_constraint/3
+msgid "has already been taken"
+msgstr ""
+
+## From Ecto.Changeset.put_change/3
+msgid "is invalid"
+msgstr ""
+
+## From Ecto.Changeset.validate_acceptance/3
+msgid "must be accepted"
+msgstr ""
+
+## From Ecto.Changeset.validate_format/3
+msgid "has invalid format"
+msgstr ""
+
+## From Ecto.Changeset.validate_subset/3
+msgid "has an invalid entry"
+msgstr ""
+
+## From Ecto.Changeset.validate_exclusion/3
+msgid "is reserved"
+msgstr ""
+
+## From Ecto.Changeset.validate_confirmation/3
+msgid "does not match confirmation"
+msgstr ""
+
+## From Ecto.Changeset.no_assoc_constraint/3
+msgid "is still associated with this entry"
+msgstr ""
+
+msgid "are still associated with this entry"
+msgstr ""
+
+## From Ecto.Changeset.validate_length/3
+msgid "should have %{count} item(s)"
+msgid_plural "should have %{count} item(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should be %{count} character(s)"
+msgid_plural "should be %{count} character(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should be %{count} byte(s)"
+msgid_plural "should be %{count} byte(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should have at least %{count} item(s)"
+msgid_plural "should have at least %{count} item(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should be at least %{count} character(s)"
+msgid_plural "should be at least %{count} character(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should be at least %{count} byte(s)"
+msgid_plural "should be at least %{count} byte(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should have at most %{count} item(s)"
+msgid_plural "should have at most %{count} item(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should be at most %{count} character(s)"
+msgid_plural "should be at most %{count} character(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should be at most %{count} byte(s)"
+msgid_plural "should be at most %{count} byte(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+## From Ecto.Changeset.validate_number/3
+msgid "must be less than %{number}"
+msgstr ""
+
+msgid "must be greater than %{number}"
+msgstr ""
+
+msgid "must be less than or equal to %{number}"
+msgstr ""
+
+msgid "must be greater than or equal to %{number}"
+msgstr ""
+
+msgid "must be equal to %{number}"
+msgstr ""
diff --git a/Elixir/notes_app/priv/repo/migrations/.formatter.exs b/Elixir/notes_app/priv/repo/migrations/.formatter.exs
new file mode 100644
index 0000000..49f9151
--- /dev/null
+++ b/Elixir/notes_app/priv/repo/migrations/.formatter.exs
@@ -0,0 +1,4 @@
+[
+ import_deps: [:ecto_sql],
+ inputs: ["*.exs"]
+]
diff --git a/Elixir/notes_app/priv/repo/migrations/20240903074433_create_users_auth_tables.exs b/Elixir/notes_app/priv/repo/migrations/20240903074433_create_users_auth_tables.exs
new file mode 100644
index 0000000..38171d2
--- /dev/null
+++ b/Elixir/notes_app/priv/repo/migrations/20240903074433_create_users_auth_tables.exs
@@ -0,0 +1,27 @@
+defmodule NotesApp.Repo.Migrations.CreateUsersAuthTables do
+ use Ecto.Migration
+
+ def change do
+ create table(:users) do
+ add :email, :string, null: false, collate: :nocase
+ add :hashed_password, :string, null: false
+ add :confirmed_at, :utc_datetime
+
+ timestamps(type: :utc_datetime)
+ end
+
+ create unique_index(:users, [:email])
+
+ create table(:users_tokens) do
+ add :user_id, references(:users, on_delete: :delete_all), null: false
+ add :token, :binary, null: false, size: 32
+ add :context, :string, null: false
+ add :sent_to, :string
+
+ timestamps(type: :utc_datetime, updated_at: false)
+ end
+
+ create index(:users_tokens, [:user_id])
+ create unique_index(:users_tokens, [:context, :token])
+ end
+end
diff --git a/Elixir/notes_app/priv/repo/seeds.exs b/Elixir/notes_app/priv/repo/seeds.exs
new file mode 100644
index 0000000..1caec55
--- /dev/null
+++ b/Elixir/notes_app/priv/repo/seeds.exs
@@ -0,0 +1,11 @@
+# Script for populating the database. You can run it as:
+#
+# mix run priv/repo/seeds.exs
+#
+# Inside the script, you can read and write to any of your
+# repositories directly:
+#
+# NotesApp.Repo.insert!(%NotesApp.SomeSchema{})
+#
+# We recommend using the bang functions (`insert!`, `update!`
+# and so on) as they will fail if something goes wrong.
diff --git a/Elixir/notes_app/priv/static/favicon.ico b/Elixir/notes_app/priv/static/favicon.ico
new file mode 100644
index 0000000..7f372bf
Binary files /dev/null and b/Elixir/notes_app/priv/static/favicon.ico differ
diff --git a/Elixir/notes_app/priv/static/images/logo.svg b/Elixir/notes_app/priv/static/images/logo.svg
new file mode 100644
index 0000000..9f26bab
--- /dev/null
+++ b/Elixir/notes_app/priv/static/images/logo.svg
@@ -0,0 +1,6 @@
+
diff --git a/Elixir/notes_app/priv/static/robots.txt b/Elixir/notes_app/priv/static/robots.txt
new file mode 100644
index 0000000..26e06b5
--- /dev/null
+++ b/Elixir/notes_app/priv/static/robots.txt
@@ -0,0 +1,5 @@
+# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
+#
+# To ban all spiders from the entire site uncomment the next two lines:
+# User-agent: *
+# Disallow: /
diff --git a/Elixir/notes_app/test/notes_app/accounts_test.exs b/Elixir/notes_app/test/notes_app/accounts_test.exs
new file mode 100644
index 0000000..4402677
--- /dev/null
+++ b/Elixir/notes_app/test/notes_app/accounts_test.exs
@@ -0,0 +1,508 @@
+defmodule NotesApp.AccountsTest do
+ use NotesApp.DataCase
+
+ alias NotesApp.Accounts
+
+ import NotesApp.AccountsFixtures
+ alias NotesApp.Accounts.{User, UserToken}
+
+ describe "get_user_by_email/1" do
+ test "does not return the user if the email does not exist" do
+ refute Accounts.get_user_by_email("unknown@example.com")
+ end
+
+ test "returns the user if the email exists" do
+ %{id: id} = user = user_fixture()
+ assert %User{id: ^id} = Accounts.get_user_by_email(user.email)
+ end
+ end
+
+ describe "get_user_by_email_and_password/2" do
+ test "does not return the user if the email does not exist" do
+ refute Accounts.get_user_by_email_and_password("unknown@example.com", "hello world!")
+ end
+
+ test "does not return the user if the password is not valid" do
+ user = user_fixture()
+ refute Accounts.get_user_by_email_and_password(user.email, "invalid")
+ end
+
+ test "returns the user if the email and password are valid" do
+ %{id: id} = user = user_fixture()
+
+ assert %User{id: ^id} =
+ Accounts.get_user_by_email_and_password(user.email, valid_user_password())
+ end
+ end
+
+ describe "get_user!/1" do
+ test "raises if id is invalid" do
+ assert_raise Ecto.NoResultsError, fn ->
+ Accounts.get_user!(-1)
+ end
+ end
+
+ test "returns the user with the given id" do
+ %{id: id} = user = user_fixture()
+ assert %User{id: ^id} = Accounts.get_user!(user.id)
+ end
+ end
+
+ describe "register_user/1" do
+ test "requires email and password to be set" do
+ {:error, changeset} = Accounts.register_user(%{})
+
+ assert %{
+ password: ["can't be blank"],
+ email: ["can't be blank"]
+ } = errors_on(changeset)
+ end
+
+ test "validates email and password when given" do
+ {:error, changeset} = Accounts.register_user(%{email: "not valid", password: "not valid"})
+
+ assert %{
+ email: ["must have the @ sign and no spaces"],
+ password: ["should be at least 12 character(s)"]
+ } = errors_on(changeset)
+ end
+
+ test "validates maximum values for email and password for security" do
+ too_long = String.duplicate("db", 100)
+ {:error, changeset} = Accounts.register_user(%{email: too_long, password: too_long})
+ assert "should be at most 160 character(s)" in errors_on(changeset).email
+ assert "should be at most 72 character(s)" in errors_on(changeset).password
+ end
+
+ test "validates email uniqueness" do
+ %{email: email} = user_fixture()
+ {:error, changeset} = Accounts.register_user(%{email: email})
+ assert "has already been taken" in errors_on(changeset).email
+
+ # Now try with the upper cased email too, to check that email case is ignored.
+ {:error, changeset} = Accounts.register_user(%{email: String.upcase(email)})
+ assert "has already been taken" in errors_on(changeset).email
+ end
+
+ test "registers users with a hashed password" do
+ email = unique_user_email()
+ {:ok, user} = Accounts.register_user(valid_user_attributes(email: email))
+ assert user.email == email
+ assert is_binary(user.hashed_password)
+ assert is_nil(user.confirmed_at)
+ assert is_nil(user.password)
+ end
+ end
+
+ describe "change_user_registration/2" do
+ test "returns a changeset" do
+ assert %Ecto.Changeset{} = changeset = Accounts.change_user_registration(%User{})
+ assert changeset.required == [:password, :email]
+ end
+
+ test "allows fields to be set" do
+ email = unique_user_email()
+ password = valid_user_password()
+
+ changeset =
+ Accounts.change_user_registration(
+ %User{},
+ valid_user_attributes(email: email, password: password)
+ )
+
+ assert changeset.valid?
+ assert get_change(changeset, :email) == email
+ assert get_change(changeset, :password) == password
+ assert is_nil(get_change(changeset, :hashed_password))
+ end
+ end
+
+ describe "change_user_email/2" do
+ test "returns a user changeset" do
+ assert %Ecto.Changeset{} = changeset = Accounts.change_user_email(%User{})
+ assert changeset.required == [:email]
+ end
+ end
+
+ describe "apply_user_email/3" do
+ setup do
+ %{user: user_fixture()}
+ end
+
+ test "requires email to change", %{user: user} do
+ {:error, changeset} = Accounts.apply_user_email(user, valid_user_password(), %{})
+ assert %{email: ["did not change"]} = errors_on(changeset)
+ end
+
+ test "validates email", %{user: user} do
+ {:error, changeset} =
+ Accounts.apply_user_email(user, valid_user_password(), %{email: "not valid"})
+
+ assert %{email: ["must have the @ sign and no spaces"]} = errors_on(changeset)
+ end
+
+ test "validates maximum value for email for security", %{user: user} do
+ too_long = String.duplicate("db", 100)
+
+ {:error, changeset} =
+ Accounts.apply_user_email(user, valid_user_password(), %{email: too_long})
+
+ assert "should be at most 160 character(s)" in errors_on(changeset).email
+ end
+
+ test "validates email uniqueness", %{user: user} do
+ %{email: email} = user_fixture()
+ password = valid_user_password()
+
+ {:error, changeset} = Accounts.apply_user_email(user, password, %{email: email})
+
+ assert "has already been taken" in errors_on(changeset).email
+ end
+
+ test "validates current password", %{user: user} do
+ {:error, changeset} =
+ Accounts.apply_user_email(user, "invalid", %{email: unique_user_email()})
+
+ assert %{current_password: ["is not valid"]} = errors_on(changeset)
+ end
+
+ test "applies the email without persisting it", %{user: user} do
+ email = unique_user_email()
+ {:ok, user} = Accounts.apply_user_email(user, valid_user_password(), %{email: email})
+ assert user.email == email
+ assert Accounts.get_user!(user.id).email != email
+ end
+ end
+
+ describe "deliver_user_update_email_instructions/3" do
+ setup do
+ %{user: user_fixture()}
+ end
+
+ test "sends token through notification", %{user: user} do
+ token =
+ extract_user_token(fn url ->
+ Accounts.deliver_user_update_email_instructions(user, "current@example.com", url)
+ end)
+
+ {:ok, token} = Base.url_decode64(token, padding: false)
+ assert user_token = Repo.get_by(UserToken, token: :crypto.hash(:sha256, token))
+ assert user_token.user_id == user.id
+ assert user_token.sent_to == user.email
+ assert user_token.context == "change:current@example.com"
+ end
+ end
+
+ describe "update_user_email/2" do
+ setup do
+ user = user_fixture()
+ email = unique_user_email()
+
+ token =
+ extract_user_token(fn url ->
+ Accounts.deliver_user_update_email_instructions(%{user | email: email}, user.email, url)
+ end)
+
+ %{user: user, token: token, email: email}
+ end
+
+ test "updates the email with a valid token", %{user: user, token: token, email: email} do
+ assert Accounts.update_user_email(user, token) == :ok
+ changed_user = Repo.get!(User, user.id)
+ assert changed_user.email != user.email
+ assert changed_user.email == email
+ assert changed_user.confirmed_at
+ assert changed_user.confirmed_at != user.confirmed_at
+ refute Repo.get_by(UserToken, user_id: user.id)
+ end
+
+ test "does not update email with invalid token", %{user: user} do
+ assert Accounts.update_user_email(user, "oops") == :error
+ assert Repo.get!(User, user.id).email == user.email
+ assert Repo.get_by(UserToken, user_id: user.id)
+ end
+
+ test "does not update email if user email changed", %{user: user, token: token} do
+ assert Accounts.update_user_email(%{user | email: "current@example.com"}, token) == :error
+ assert Repo.get!(User, user.id).email == user.email
+ assert Repo.get_by(UserToken, user_id: user.id)
+ end
+
+ test "does not update email if token expired", %{user: user, token: token} do
+ {1, nil} = Repo.update_all(UserToken, set: [inserted_at: ~N[2020-01-01 00:00:00]])
+ assert Accounts.update_user_email(user, token) == :error
+ assert Repo.get!(User, user.id).email == user.email
+ assert Repo.get_by(UserToken, user_id: user.id)
+ end
+ end
+
+ describe "change_user_password/2" do
+ test "returns a user changeset" do
+ assert %Ecto.Changeset{} = changeset = Accounts.change_user_password(%User{})
+ assert changeset.required == [:password]
+ end
+
+ test "allows fields to be set" do
+ changeset =
+ Accounts.change_user_password(%User{}, %{
+ "password" => "new valid password"
+ })
+
+ assert changeset.valid?
+ assert get_change(changeset, :password) == "new valid password"
+ assert is_nil(get_change(changeset, :hashed_password))
+ end
+ end
+
+ describe "update_user_password/3" do
+ setup do
+ %{user: user_fixture()}
+ end
+
+ test "validates password", %{user: user} do
+ {:error, changeset} =
+ Accounts.update_user_password(user, valid_user_password(), %{
+ password: "not valid",
+ password_confirmation: "another"
+ })
+
+ assert %{
+ password: ["should be at least 12 character(s)"],
+ password_confirmation: ["does not match password"]
+ } = errors_on(changeset)
+ end
+
+ test "validates maximum values for password for security", %{user: user} do
+ too_long = String.duplicate("db", 100)
+
+ {:error, changeset} =
+ Accounts.update_user_password(user, valid_user_password(), %{password: too_long})
+
+ assert "should be at most 72 character(s)" in errors_on(changeset).password
+ end
+
+ test "validates current password", %{user: user} do
+ {:error, changeset} =
+ Accounts.update_user_password(user, "invalid", %{password: valid_user_password()})
+
+ assert %{current_password: ["is not valid"]} = errors_on(changeset)
+ end
+
+ test "updates the password", %{user: user} do
+ {:ok, user} =
+ Accounts.update_user_password(user, valid_user_password(), %{
+ password: "new valid password"
+ })
+
+ assert is_nil(user.password)
+ assert Accounts.get_user_by_email_and_password(user.email, "new valid password")
+ end
+
+ test "deletes all tokens for the given user", %{user: user} do
+ _ = Accounts.generate_user_session_token(user)
+
+ {:ok, _} =
+ Accounts.update_user_password(user, valid_user_password(), %{
+ password: "new valid password"
+ })
+
+ refute Repo.get_by(UserToken, user_id: user.id)
+ end
+ end
+
+ describe "generate_user_session_token/1" do
+ setup do
+ %{user: user_fixture()}
+ end
+
+ test "generates a token", %{user: user} do
+ token = Accounts.generate_user_session_token(user)
+ assert user_token = Repo.get_by(UserToken, token: token)
+ assert user_token.context == "session"
+
+ # Creating the same token for another user should fail
+ assert_raise Ecto.ConstraintError, fn ->
+ Repo.insert!(%UserToken{
+ token: user_token.token,
+ user_id: user_fixture().id,
+ context: "session"
+ })
+ end
+ end
+ end
+
+ describe "get_user_by_session_token/1" do
+ setup do
+ user = user_fixture()
+ token = Accounts.generate_user_session_token(user)
+ %{user: user, token: token}
+ end
+
+ test "returns user by token", %{user: user, token: token} do
+ assert session_user = Accounts.get_user_by_session_token(token)
+ assert session_user.id == user.id
+ end
+
+ test "does not return user for invalid token" do
+ refute Accounts.get_user_by_session_token("oops")
+ end
+
+ test "does not return user for expired token", %{token: token} do
+ {1, nil} = Repo.update_all(UserToken, set: [inserted_at: ~N[2020-01-01 00:00:00]])
+ refute Accounts.get_user_by_session_token(token)
+ end
+ end
+
+ describe "delete_user_session_token/1" do
+ test "deletes the token" do
+ user = user_fixture()
+ token = Accounts.generate_user_session_token(user)
+ assert Accounts.delete_user_session_token(token) == :ok
+ refute Accounts.get_user_by_session_token(token)
+ end
+ end
+
+ describe "deliver_user_confirmation_instructions/2" do
+ setup do
+ %{user: user_fixture()}
+ end
+
+ test "sends token through notification", %{user: user} do
+ token =
+ extract_user_token(fn url ->
+ Accounts.deliver_user_confirmation_instructions(user, url)
+ end)
+
+ {:ok, token} = Base.url_decode64(token, padding: false)
+ assert user_token = Repo.get_by(UserToken, token: :crypto.hash(:sha256, token))
+ assert user_token.user_id == user.id
+ assert user_token.sent_to == user.email
+ assert user_token.context == "confirm"
+ end
+ end
+
+ describe "confirm_user/1" do
+ setup do
+ user = user_fixture()
+
+ token =
+ extract_user_token(fn url ->
+ Accounts.deliver_user_confirmation_instructions(user, url)
+ end)
+
+ %{user: user, token: token}
+ end
+
+ test "confirms the email with a valid token", %{user: user, token: token} do
+ assert {:ok, confirmed_user} = Accounts.confirm_user(token)
+ assert confirmed_user.confirmed_at
+ assert confirmed_user.confirmed_at != user.confirmed_at
+ assert Repo.get!(User, user.id).confirmed_at
+ refute Repo.get_by(UserToken, user_id: user.id)
+ end
+
+ test "does not confirm with invalid token", %{user: user} do
+ assert Accounts.confirm_user("oops") == :error
+ refute Repo.get!(User, user.id).confirmed_at
+ assert Repo.get_by(UserToken, user_id: user.id)
+ end
+
+ test "does not confirm email if token expired", %{user: user, token: token} do
+ {1, nil} = Repo.update_all(UserToken, set: [inserted_at: ~N[2020-01-01 00:00:00]])
+ assert Accounts.confirm_user(token) == :error
+ refute Repo.get!(User, user.id).confirmed_at
+ assert Repo.get_by(UserToken, user_id: user.id)
+ end
+ end
+
+ describe "deliver_user_reset_password_instructions/2" do
+ setup do
+ %{user: user_fixture()}
+ end
+
+ test "sends token through notification", %{user: user} do
+ token =
+ extract_user_token(fn url ->
+ Accounts.deliver_user_reset_password_instructions(user, url)
+ end)
+
+ {:ok, token} = Base.url_decode64(token, padding: false)
+ assert user_token = Repo.get_by(UserToken, token: :crypto.hash(:sha256, token))
+ assert user_token.user_id == user.id
+ assert user_token.sent_to == user.email
+ assert user_token.context == "reset_password"
+ end
+ end
+
+ describe "get_user_by_reset_password_token/1" do
+ setup do
+ user = user_fixture()
+
+ token =
+ extract_user_token(fn url ->
+ Accounts.deliver_user_reset_password_instructions(user, url)
+ end)
+
+ %{user: user, token: token}
+ end
+
+ test "returns the user with valid token", %{user: %{id: id}, token: token} do
+ assert %User{id: ^id} = Accounts.get_user_by_reset_password_token(token)
+ assert Repo.get_by(UserToken, user_id: id)
+ end
+
+ test "does not return the user with invalid token", %{user: user} do
+ refute Accounts.get_user_by_reset_password_token("oops")
+ assert Repo.get_by(UserToken, user_id: user.id)
+ end
+
+ test "does not return the user if token expired", %{user: user, token: token} do
+ {1, nil} = Repo.update_all(UserToken, set: [inserted_at: ~N[2020-01-01 00:00:00]])
+ refute Accounts.get_user_by_reset_password_token(token)
+ assert Repo.get_by(UserToken, user_id: user.id)
+ end
+ end
+
+ describe "reset_user_password/2" do
+ setup do
+ %{user: user_fixture()}
+ end
+
+ test "validates password", %{user: user} do
+ {:error, changeset} =
+ Accounts.reset_user_password(user, %{
+ password: "not valid",
+ password_confirmation: "another"
+ })
+
+ assert %{
+ password: ["should be at least 12 character(s)"],
+ password_confirmation: ["does not match password"]
+ } = errors_on(changeset)
+ end
+
+ test "validates maximum values for password for security", %{user: user} do
+ too_long = String.duplicate("db", 100)
+ {:error, changeset} = Accounts.reset_user_password(user, %{password: too_long})
+ assert "should be at most 72 character(s)" in errors_on(changeset).password
+ end
+
+ test "updates the password", %{user: user} do
+ {:ok, updated_user} = Accounts.reset_user_password(user, %{password: "new valid password"})
+ assert is_nil(updated_user.password)
+ assert Accounts.get_user_by_email_and_password(user.email, "new valid password")
+ end
+
+ test "deletes all tokens for the given user", %{user: user} do
+ _ = Accounts.generate_user_session_token(user)
+ {:ok, _} = Accounts.reset_user_password(user, %{password: "new valid password"})
+ refute Repo.get_by(UserToken, user_id: user.id)
+ end
+ end
+
+ describe "inspect/2 for the User module" do
+ test "does not include password" do
+ refute inspect(%User{password: "123456"}) =~ "password: \"123456\""
+ end
+ end
+end
diff --git a/Elixir/notes_app/test/notes_app_web/controllers/error_html_test.exs b/Elixir/notes_app/test/notes_app_web/controllers/error_html_test.exs
new file mode 100644
index 0000000..8fb1bf2
--- /dev/null
+++ b/Elixir/notes_app/test/notes_app_web/controllers/error_html_test.exs
@@ -0,0 +1,14 @@
+defmodule NotesAppWeb.ErrorHTMLTest do
+ use NotesAppWeb.ConnCase, async: true
+
+ # Bring render_to_string/4 for testing custom views
+ import Phoenix.Template
+
+ test "renders 404.html" do
+ assert render_to_string(NotesAppWeb.ErrorHTML, "404", "html", []) == "Not Found"
+ end
+
+ test "renders 500.html" do
+ assert render_to_string(NotesAppWeb.ErrorHTML, "500", "html", []) == "Internal Server Error"
+ end
+end
diff --git a/Elixir/notes_app/test/notes_app_web/controllers/error_json_test.exs b/Elixir/notes_app/test/notes_app_web/controllers/error_json_test.exs
new file mode 100644
index 0000000..5dab933
--- /dev/null
+++ b/Elixir/notes_app/test/notes_app_web/controllers/error_json_test.exs
@@ -0,0 +1,12 @@
+defmodule NotesAppWeb.ErrorJSONTest do
+ use NotesAppWeb.ConnCase, async: true
+
+ test "renders 404" do
+ assert NotesAppWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}}
+ end
+
+ test "renders 500" do
+ assert NotesAppWeb.ErrorJSON.render("500.json", %{}) ==
+ %{errors: %{detail: "Internal Server Error"}}
+ end
+end
diff --git a/Elixir/notes_app/test/notes_app_web/controllers/page_controller_test.exs b/Elixir/notes_app/test/notes_app_web/controllers/page_controller_test.exs
new file mode 100644
index 0000000..622996d
--- /dev/null
+++ b/Elixir/notes_app/test/notes_app_web/controllers/page_controller_test.exs
@@ -0,0 +1,8 @@
+defmodule NotesAppWeb.PageControllerTest do
+ use NotesAppWeb.ConnCase
+
+ test "GET /", %{conn: conn} do
+ conn = get(conn, ~p"/")
+ assert html_response(conn, 200) =~ "Peace of mind from prototype to production"
+ end
+end
diff --git a/Elixir/notes_app/test/notes_app_web/controllers/user_session_controller_test.exs b/Elixir/notes_app/test/notes_app_web/controllers/user_session_controller_test.exs
new file mode 100644
index 0000000..9823e8d
--- /dev/null
+++ b/Elixir/notes_app/test/notes_app_web/controllers/user_session_controller_test.exs
@@ -0,0 +1,113 @@
+defmodule NotesAppWeb.UserSessionControllerTest do
+ use NotesAppWeb.ConnCase
+
+ import NotesApp.AccountsFixtures
+
+ setup do
+ %{user: user_fixture()}
+ end
+
+ describe "POST /users/log_in" do
+ test "logs the user in", %{conn: conn, user: user} do
+ conn =
+ post(conn, ~p"/users/log_in", %{
+ "user" => %{"email" => user.email, "password" => valid_user_password()}
+ })
+
+ assert get_session(conn, :user_token)
+ assert redirected_to(conn) == ~p"/"
+
+ # Now do a logged in request and assert on the menu
+ conn = get(conn, ~p"/")
+ response = html_response(conn, 200)
+ assert response =~ user.email
+ assert response =~ ~p"/users/settings"
+ assert response =~ ~p"/users/log_out"
+ end
+
+ test "logs the user in with remember me", %{conn: conn, user: user} do
+ conn =
+ post(conn, ~p"/users/log_in", %{
+ "user" => %{
+ "email" => user.email,
+ "password" => valid_user_password(),
+ "remember_me" => "true"
+ }
+ })
+
+ assert conn.resp_cookies["_notes_app_web_user_remember_me"]
+ assert redirected_to(conn) == ~p"/"
+ end
+
+ test "logs the user in with return to", %{conn: conn, user: user} do
+ conn =
+ conn
+ |> init_test_session(user_return_to: "/foo/bar")
+ |> post(~p"/users/log_in", %{
+ "user" => %{
+ "email" => user.email,
+ "password" => valid_user_password()
+ }
+ })
+
+ assert redirected_to(conn) == "/foo/bar"
+ assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Welcome back!"
+ end
+
+ test "login following registration", %{conn: conn, user: user} do
+ conn =
+ conn
+ |> post(~p"/users/log_in", %{
+ "_action" => "registered",
+ "user" => %{
+ "email" => user.email,
+ "password" => valid_user_password()
+ }
+ })
+
+ assert redirected_to(conn) == ~p"/"
+ assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Account created successfully"
+ end
+
+ test "login following password update", %{conn: conn, user: user} do
+ conn =
+ conn
+ |> post(~p"/users/log_in", %{
+ "_action" => "password_updated",
+ "user" => %{
+ "email" => user.email,
+ "password" => valid_user_password()
+ }
+ })
+
+ assert redirected_to(conn) == ~p"/users/settings"
+ assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Password updated successfully"
+ end
+
+ test "redirects to login page with invalid credentials", %{conn: conn} do
+ conn =
+ post(conn, ~p"/users/log_in", %{
+ "user" => %{"email" => "invalid@email.com", "password" => "invalid_password"}
+ })
+
+ assert Phoenix.Flash.get(conn.assigns.flash, :error) == "Invalid email or password"
+ assert redirected_to(conn) == ~p"/users/log_in"
+ end
+ end
+
+ describe "DELETE /users/log_out" do
+ test "logs the user out", %{conn: conn, user: user} do
+ conn = conn |> log_in_user(user) |> delete(~p"/users/log_out")
+ assert redirected_to(conn) == ~p"/"
+ refute get_session(conn, :user_token)
+ assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Logged out successfully"
+ end
+
+ test "succeeds even if the user is not logged in", %{conn: conn} do
+ conn = delete(conn, ~p"/users/log_out")
+ assert redirected_to(conn) == ~p"/"
+ refute get_session(conn, :user_token)
+ assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Logged out successfully"
+ end
+ end
+end
diff --git a/Elixir/notes_app/test/notes_app_web/live/user_confirmation_instructions_live_test.exs b/Elixir/notes_app/test/notes_app_web/live/user_confirmation_instructions_live_test.exs
new file mode 100644
index 0000000..8185d43
--- /dev/null
+++ b/Elixir/notes_app/test/notes_app_web/live/user_confirmation_instructions_live_test.exs
@@ -0,0 +1,67 @@
+defmodule NotesAppWeb.UserConfirmationInstructionsLiveTest do
+ use NotesAppWeb.ConnCase
+
+ import Phoenix.LiveViewTest
+ import NotesApp.AccountsFixtures
+
+ alias NotesApp.Accounts
+ alias NotesApp.Repo
+
+ setup do
+ %{user: user_fixture()}
+ end
+
+ describe "Resend confirmation" do
+ test "renders the resend confirmation page", %{conn: conn} do
+ {:ok, _lv, html} = live(conn, ~p"/users/confirm")
+ assert html =~ "Resend confirmation instructions"
+ end
+
+ test "sends a new confirmation token", %{conn: conn, user: user} do
+ {:ok, lv, _html} = live(conn, ~p"/users/confirm")
+
+ {:ok, conn} =
+ lv
+ |> form("#resend_confirmation_form", user: %{email: user.email})
+ |> render_submit()
+ |> follow_redirect(conn, ~p"/")
+
+ assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
+ "If your email is in our system"
+
+ assert Repo.get_by!(Accounts.UserToken, user_id: user.id).context == "confirm"
+ end
+
+ test "does not send confirmation token if user is confirmed", %{conn: conn, user: user} do
+ Repo.update!(Accounts.User.confirm_changeset(user))
+
+ {:ok, lv, _html} = live(conn, ~p"/users/confirm")
+
+ {:ok, conn} =
+ lv
+ |> form("#resend_confirmation_form", user: %{email: user.email})
+ |> render_submit()
+ |> follow_redirect(conn, ~p"/")
+
+ assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
+ "If your email is in our system"
+
+ refute Repo.get_by(Accounts.UserToken, user_id: user.id)
+ end
+
+ test "does not send confirmation token if email is invalid", %{conn: conn} do
+ {:ok, lv, _html} = live(conn, ~p"/users/confirm")
+
+ {:ok, conn} =
+ lv
+ |> form("#resend_confirmation_form", user: %{email: "unknown@example.com"})
+ |> render_submit()
+ |> follow_redirect(conn, ~p"/")
+
+ assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
+ "If your email is in our system"
+
+ assert Repo.all(Accounts.UserToken) == []
+ end
+ end
+end
diff --git a/Elixir/notes_app/test/notes_app_web/live/user_confirmation_live_test.exs b/Elixir/notes_app/test/notes_app_web/live/user_confirmation_live_test.exs
new file mode 100644
index 0000000..548c45d
--- /dev/null
+++ b/Elixir/notes_app/test/notes_app_web/live/user_confirmation_live_test.exs
@@ -0,0 +1,89 @@
+defmodule NotesAppWeb.UserConfirmationLiveTest do
+ use NotesAppWeb.ConnCase
+
+ import Phoenix.LiveViewTest
+ import NotesApp.AccountsFixtures
+
+ alias NotesApp.Accounts
+ alias NotesApp.Repo
+
+ setup do
+ %{user: user_fixture()}
+ end
+
+ describe "Confirm user" do
+ test "renders confirmation page", %{conn: conn} do
+ {:ok, _lv, html} = live(conn, ~p"/users/confirm/some-token")
+ assert html =~ "Confirm Account"
+ end
+
+ test "confirms the given token once", %{conn: conn, user: user} do
+ token =
+ extract_user_token(fn url ->
+ Accounts.deliver_user_confirmation_instructions(user, url)
+ end)
+
+ {:ok, lv, _html} = live(conn, ~p"/users/confirm/#{token}")
+
+ result =
+ lv
+ |> form("#confirmation_form")
+ |> render_submit()
+ |> follow_redirect(conn, "/")
+
+ assert {:ok, conn} = result
+
+ assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
+ "User confirmed successfully"
+
+ assert Accounts.get_user!(user.id).confirmed_at
+ refute get_session(conn, :user_token)
+ assert Repo.all(Accounts.UserToken) == []
+
+ # when not logged in
+ {:ok, lv, _html} = live(conn, ~p"/users/confirm/#{token}")
+
+ result =
+ lv
+ |> form("#confirmation_form")
+ |> render_submit()
+ |> follow_redirect(conn, "/")
+
+ assert {:ok, conn} = result
+
+ assert Phoenix.Flash.get(conn.assigns.flash, :error) =~
+ "User confirmation link is invalid or it has expired"
+
+ # when logged in
+ conn =
+ build_conn()
+ |> log_in_user(user)
+
+ {:ok, lv, _html} = live(conn, ~p"/users/confirm/#{token}")
+
+ result =
+ lv
+ |> form("#confirmation_form")
+ |> render_submit()
+ |> follow_redirect(conn, "/")
+
+ assert {:ok, conn} = result
+ refute Phoenix.Flash.get(conn.assigns.flash, :error)
+ end
+
+ test "does not confirm email with invalid token", %{conn: conn, user: user} do
+ {:ok, lv, _html} = live(conn, ~p"/users/confirm/invalid-token")
+
+ {:ok, conn} =
+ lv
+ |> form("#confirmation_form")
+ |> render_submit()
+ |> follow_redirect(conn, ~p"/")
+
+ assert Phoenix.Flash.get(conn.assigns.flash, :error) =~
+ "User confirmation link is invalid or it has expired"
+
+ refute Accounts.get_user!(user.id).confirmed_at
+ end
+ end
+end
diff --git a/Elixir/notes_app/test/notes_app_web/live/user_forgot_password_live_test.exs b/Elixir/notes_app/test/notes_app_web/live/user_forgot_password_live_test.exs
new file mode 100644
index 0000000..7d516c7
--- /dev/null
+++ b/Elixir/notes_app/test/notes_app_web/live/user_forgot_password_live_test.exs
@@ -0,0 +1,63 @@
+defmodule NotesAppWeb.UserForgotPasswordLiveTest do
+ use NotesAppWeb.ConnCase
+
+ import Phoenix.LiveViewTest
+ import NotesApp.AccountsFixtures
+
+ alias NotesApp.Accounts
+ alias NotesApp.Repo
+
+ describe "Forgot password page" do
+ test "renders email page", %{conn: conn} do
+ {:ok, lv, html} = live(conn, ~p"/users/reset_password")
+
+ assert html =~ "Forgot your password?"
+ assert has_element?(lv, ~s|a[href="#{~p"/users/register"}"]|, "Register")
+ assert has_element?(lv, ~s|a[href="#{~p"/users/log_in"}"]|, "Log in")
+ end
+
+ test "redirects if already logged in", %{conn: conn} do
+ result =
+ conn
+ |> log_in_user(user_fixture())
+ |> live(~p"/users/reset_password")
+ |> follow_redirect(conn, ~p"/")
+
+ assert {:ok, _conn} = result
+ end
+ end
+
+ describe "Reset link" do
+ setup do
+ %{user: user_fixture()}
+ end
+
+ test "sends a new reset password token", %{conn: conn, user: user} do
+ {:ok, lv, _html} = live(conn, ~p"/users/reset_password")
+
+ {:ok, conn} =
+ lv
+ |> form("#reset_password_form", user: %{"email" => user.email})
+ |> render_submit()
+ |> follow_redirect(conn, "/")
+
+ assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "If your email is in our system"
+
+ assert Repo.get_by!(Accounts.UserToken, user_id: user.id).context ==
+ "reset_password"
+ end
+
+ test "does not send reset password token if email is invalid", %{conn: conn} do
+ {:ok, lv, _html} = live(conn, ~p"/users/reset_password")
+
+ {:ok, conn} =
+ lv
+ |> form("#reset_password_form", user: %{"email" => "unknown@example.com"})
+ |> render_submit()
+ |> follow_redirect(conn, "/")
+
+ assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "If your email is in our system"
+ assert Repo.all(Accounts.UserToken) == []
+ end
+ end
+end
diff --git a/Elixir/notes_app/test/notes_app_web/live/user_login_live_test.exs b/Elixir/notes_app/test/notes_app_web/live/user_login_live_test.exs
new file mode 100644
index 0000000..b9f055f
--- /dev/null
+++ b/Elixir/notes_app/test/notes_app_web/live/user_login_live_test.exs
@@ -0,0 +1,87 @@
+defmodule NotesAppWeb.UserLoginLiveTest do
+ use NotesAppWeb.ConnCase
+
+ import Phoenix.LiveViewTest
+ import NotesApp.AccountsFixtures
+
+ describe "Log in page" do
+ test "renders log in page", %{conn: conn} do
+ {:ok, _lv, html} = live(conn, ~p"/users/log_in")
+
+ assert html =~ "Log in"
+ assert html =~ "Register"
+ assert html =~ "Forgot your password?"
+ end
+
+ test "redirects if already logged in", %{conn: conn} do
+ result =
+ conn
+ |> log_in_user(user_fixture())
+ |> live(~p"/users/log_in")
+ |> follow_redirect(conn, "/")
+
+ assert {:ok, _conn} = result
+ end
+ end
+
+ describe "user login" do
+ test "redirects if user login with valid credentials", %{conn: conn} do
+ password = "123456789abcd"
+ user = user_fixture(%{password: password})
+
+ {:ok, lv, _html} = live(conn, ~p"/users/log_in")
+
+ form =
+ form(lv, "#login_form", user: %{email: user.email, password: password, remember_me: true})
+
+ conn = submit_form(form, conn)
+
+ assert redirected_to(conn) == ~p"/"
+ end
+
+ test "redirects to login page with a flash error if there are no valid credentials", %{
+ conn: conn
+ } do
+ {:ok, lv, _html} = live(conn, ~p"/users/log_in")
+
+ form =
+ form(lv, "#login_form",
+ user: %{email: "test@email.com", password: "123456", remember_me: true}
+ )
+
+ conn = submit_form(form, conn)
+
+ assert Phoenix.Flash.get(conn.assigns.flash, :error) == "Invalid email or password"
+
+ assert redirected_to(conn) == "/users/log_in"
+ end
+ end
+
+ describe "login navigation" do
+ test "redirects to registration page when the Register button is clicked", %{conn: conn} do
+ {:ok, lv, _html} = live(conn, ~p"/users/log_in")
+
+ {:ok, _login_live, login_html} =
+ lv
+ |> element(~s|main a:fl-contains("Sign up")|)
+ |> render_click()
+ |> follow_redirect(conn, ~p"/users/register")
+
+ assert login_html =~ "Register"
+ end
+
+ test "redirects to forgot password page when the Forgot Password button is clicked", %{
+ conn: conn
+ } do
+ {:ok, lv, _html} = live(conn, ~p"/users/log_in")
+
+ {:ok, conn} =
+ lv
+ |> element(~s|main a:fl-contains("Forgot your password?")|)
+ |> render_click()
+ |> follow_redirect(conn, ~p"/users/reset_password")
+
+ assert conn.resp_body =~ "Forgot your password?"
+ end
+ end
+end
diff --git a/Elixir/notes_app/test/notes_app_web/live/user_registration_live_test.exs b/Elixir/notes_app/test/notes_app_web/live/user_registration_live_test.exs
new file mode 100644
index 0000000..c07574c
--- /dev/null
+++ b/Elixir/notes_app/test/notes_app_web/live/user_registration_live_test.exs
@@ -0,0 +1,87 @@
+defmodule NotesAppWeb.UserRegistrationLiveTest do
+ use NotesAppWeb.ConnCase
+
+ import Phoenix.LiveViewTest
+ import NotesApp.AccountsFixtures
+
+ describe "Registration page" do
+ test "renders registration page", %{conn: conn} do
+ {:ok, _lv, html} = live(conn, ~p"/users/register")
+
+ assert html =~ "Register"
+ assert html =~ "Log in"
+ end
+
+ test "redirects if already logged in", %{conn: conn} do
+ result =
+ conn
+ |> log_in_user(user_fixture())
+ |> live(~p"/users/register")
+ |> follow_redirect(conn, "/")
+
+ assert {:ok, _conn} = result
+ end
+
+ test "renders errors for invalid data", %{conn: conn} do
+ {:ok, lv, _html} = live(conn, ~p"/users/register")
+
+ result =
+ lv
+ |> element("#registration_form")
+ |> render_change(user: %{"email" => "with spaces", "password" => "too short"})
+
+ assert result =~ "Register"
+ assert result =~ "must have the @ sign and no spaces"
+ assert result =~ "should be at least 12 character"
+ end
+ end
+
+ describe "register user" do
+ test "creates account and logs the user in", %{conn: conn} do
+ {:ok, lv, _html} = live(conn, ~p"/users/register")
+
+ email = unique_user_email()
+ form = form(lv, "#registration_form", user: valid_user_attributes(email: email))
+ render_submit(form)
+ conn = follow_trigger_action(form, conn)
+
+ assert redirected_to(conn) == ~p"/"
+
+ # Now do a logged in request and assert on the menu
+ conn = get(conn, "/")
+ response = html_response(conn, 200)
+ assert response =~ email
+ assert response =~ "Settings"
+ assert response =~ "Log out"
+ end
+
+ test "renders errors for duplicated email", %{conn: conn} do
+ {:ok, lv, _html} = live(conn, ~p"/users/register")
+
+ user = user_fixture(%{email: "test@email.com"})
+
+ result =
+ lv
+ |> form("#registration_form",
+ user: %{"email" => user.email, "password" => "valid_password"}
+ )
+ |> render_submit()
+
+ assert result =~ "has already been taken"
+ end
+ end
+
+ describe "registration navigation" do
+ test "redirects to login page when the Log in button is clicked", %{conn: conn} do
+ {:ok, lv, _html} = live(conn, ~p"/users/register")
+
+ {:ok, _login_live, login_html} =
+ lv
+ |> element(~s|main a:fl-contains("Log in")|)
+ |> render_click()
+ |> follow_redirect(conn, ~p"/users/log_in")
+
+ assert login_html =~ "Log in"
+ end
+ end
+end
diff --git a/Elixir/notes_app/test/notes_app_web/live/user_reset_password_live_test.exs b/Elixir/notes_app/test/notes_app_web/live/user_reset_password_live_test.exs
new file mode 100644
index 0000000..4feba87
--- /dev/null
+++ b/Elixir/notes_app/test/notes_app_web/live/user_reset_password_live_test.exs
@@ -0,0 +1,118 @@
+defmodule NotesAppWeb.UserResetPasswordLiveTest do
+ use NotesAppWeb.ConnCase
+
+ import Phoenix.LiveViewTest
+ import NotesApp.AccountsFixtures
+
+ alias NotesApp.Accounts
+
+ setup do
+ user = user_fixture()
+
+ token =
+ extract_user_token(fn url ->
+ Accounts.deliver_user_reset_password_instructions(user, url)
+ end)
+
+ %{token: token, user: user}
+ end
+
+ describe "Reset password page" do
+ test "renders reset password with valid token", %{conn: conn, token: token} do
+ {:ok, _lv, html} = live(conn, ~p"/users/reset_password/#{token}")
+
+ assert html =~ "Reset Password"
+ end
+
+ test "does not render reset password with invalid token", %{conn: conn} do
+ {:error, {:redirect, to}} = live(conn, ~p"/users/reset_password/invalid")
+
+ assert to == %{
+ flash: %{"error" => "Reset password link is invalid or it has expired."},
+ to: ~p"/"
+ }
+ end
+
+ test "renders errors for invalid data", %{conn: conn, token: token} do
+ {:ok, lv, _html} = live(conn, ~p"/users/reset_password/#{token}")
+
+ result =
+ lv
+ |> element("#reset_password_form")
+ |> render_change(
+ user: %{"password" => "secret12", "password_confirmation" => "secret123456"}
+ )
+
+ assert result =~ "should be at least 12 character"
+ assert result =~ "does not match password"
+ end
+ end
+
+ describe "Reset Password" do
+ test "resets password once", %{conn: conn, token: token, user: user} do
+ {:ok, lv, _html} = live(conn, ~p"/users/reset_password/#{token}")
+
+ {:ok, conn} =
+ lv
+ |> form("#reset_password_form",
+ user: %{
+ "password" => "new valid password",
+ "password_confirmation" => "new valid password"
+ }
+ )
+ |> render_submit()
+ |> follow_redirect(conn, ~p"/users/log_in")
+
+ refute get_session(conn, :user_token)
+ assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Password reset successfully"
+ assert Accounts.get_user_by_email_and_password(user.email, "new valid password")
+ end
+
+ test "does not reset password on invalid data", %{conn: conn, token: token} do
+ {:ok, lv, _html} = live(conn, ~p"/users/reset_password/#{token}")
+
+ result =
+ lv
+ |> form("#reset_password_form",
+ user: %{
+ "password" => "too short",
+ "password_confirmation" => "does not match"
+ }
+ )
+ |> render_submit()
+
+ assert result =~ "Reset Password"
+ assert result =~ "should be at least 12 character(s)"
+ assert result =~ "does not match password"
+ end
+ end
+
+ describe "Reset password navigation" do
+ test "redirects to login page when the Log in button is clicked", %{conn: conn, token: token} do
+ {:ok, lv, _html} = live(conn, ~p"/users/reset_password/#{token}")
+
+ {:ok, conn} =
+ lv
+ |> element(~s|main a:fl-contains("Log in")|)
+ |> render_click()
+ |> follow_redirect(conn, ~p"/users/log_in")
+
+ assert conn.resp_body =~ "Log in"
+ end
+
+ test "redirects to registration page when the Register button is clicked", %{
+ conn: conn,
+ token: token
+ } do
+ {:ok, lv, _html} = live(conn, ~p"/users/reset_password/#{token}")
+
+ {:ok, conn} =
+ lv
+ |> element(~s|main a:fl-contains("Register")|)
+ |> render_click()
+ |> follow_redirect(conn, ~p"/users/register")
+
+ assert conn.resp_body =~ "Register"
+ end
+ end
+end
diff --git a/Elixir/notes_app/test/notes_app_web/live/user_settings_live_test.exs b/Elixir/notes_app/test/notes_app_web/live/user_settings_live_test.exs
new file mode 100644
index 0000000..153e27b
--- /dev/null
+++ b/Elixir/notes_app/test/notes_app_web/live/user_settings_live_test.exs
@@ -0,0 +1,210 @@
+defmodule NotesAppWeb.UserSettingsLiveTest do
+ use NotesAppWeb.ConnCase
+
+ alias NotesApp.Accounts
+ import Phoenix.LiveViewTest
+ import NotesApp.AccountsFixtures
+
+ describe "Settings page" do
+ test "renders settings page", %{conn: conn} do
+ {:ok, _lv, html} =
+ conn
+ |> log_in_user(user_fixture())
+ |> live(~p"/users/settings")
+
+ assert html =~ "Change Email"
+ assert html =~ "Change Password"
+ end
+
+ test "redirects if user is not logged in", %{conn: conn} do
+ assert {:error, redirect} = live(conn, ~p"/users/settings")
+
+ assert {:redirect, %{to: path, flash: flash}} = redirect
+ assert path == ~p"/users/log_in"
+ assert %{"error" => "You must log in to access this page."} = flash
+ end
+ end
+
+ describe "update email form" do
+ setup %{conn: conn} do
+ password = valid_user_password()
+ user = user_fixture(%{password: password})
+ %{conn: log_in_user(conn, user), user: user, password: password}
+ end
+
+ test "updates the user email", %{conn: conn, password: password, user: user} do
+ new_email = unique_user_email()
+
+ {:ok, lv, _html} = live(conn, ~p"/users/settings")
+
+ result =
+ lv
+ |> form("#email_form", %{
+ "current_password" => password,
+ "user" => %{"email" => new_email}
+ })
+ |> render_submit()
+
+ assert result =~ "A link to confirm your email"
+ assert Accounts.get_user_by_email(user.email)
+ end
+
+ test "renders errors with invalid data (phx-change)", %{conn: conn} do
+ {:ok, lv, _html} = live(conn, ~p"/users/settings")
+
+ result =
+ lv
+ |> element("#email_form")
+ |> render_change(%{
+ "action" => "update_email",
+ "current_password" => "invalid",
+ "user" => %{"email" => "with spaces"}
+ })
+
+ assert result =~ "Change Email"
+ assert result =~ "must have the @ sign and no spaces"
+ end
+
+ test "renders errors with invalid data (phx-submit)", %{conn: conn, user: user} do
+ {:ok, lv, _html} = live(conn, ~p"/users/settings")
+
+ result =
+ lv
+ |> form("#email_form", %{
+ "current_password" => "invalid",
+ "user" => %{"email" => user.email}
+ })
+ |> render_submit()
+
+ assert result =~ "Change Email"
+ assert result =~ "did not change"
+ assert result =~ "is not valid"
+ end
+ end
+
+ describe "update password form" do
+ setup %{conn: conn} do
+ password = valid_user_password()
+ user = user_fixture(%{password: password})
+ %{conn: log_in_user(conn, user), user: user, password: password}
+ end
+
+ test "updates the user password", %{conn: conn, user: user, password: password} do
+ new_password = valid_user_password()
+
+ {:ok, lv, _html} = live(conn, ~p"/users/settings")
+
+ form =
+ form(lv, "#password_form", %{
+ "current_password" => password,
+ "user" => %{
+ "email" => user.email,
+ "password" => new_password,
+ "password_confirmation" => new_password
+ }
+ })
+
+ render_submit(form)
+
+ new_password_conn = follow_trigger_action(form, conn)
+
+ assert redirected_to(new_password_conn) == ~p"/users/settings"
+
+ assert get_session(new_password_conn, :user_token) != get_session(conn, :user_token)
+
+ assert Phoenix.Flash.get(new_password_conn.assigns.flash, :info) =~
+ "Password updated successfully"
+
+ assert Accounts.get_user_by_email_and_password(user.email, new_password)
+ end
+
+ test "renders errors with invalid data (phx-change)", %{conn: conn} do
+ {:ok, lv, _html} = live(conn, ~p"/users/settings")
+
+ result =
+ lv
+ |> element("#password_form")
+ |> render_change(%{
+ "current_password" => "invalid",
+ "user" => %{
+ "password" => "too short",
+ "password_confirmation" => "does not match"
+ }
+ })
+
+ assert result =~ "Change Password"
+ assert result =~ "should be at least 12 character(s)"
+ assert result =~ "does not match password"
+ end
+
+ test "renders errors with invalid data (phx-submit)", %{conn: conn} do
+ {:ok, lv, _html} = live(conn, ~p"/users/settings")
+
+ result =
+ lv
+ |> form("#password_form", %{
+ "current_password" => "invalid",
+ "user" => %{
+ "password" => "too short",
+ "password_confirmation" => "does not match"
+ }
+ })
+ |> render_submit()
+
+ assert result =~ "Change Password"
+ assert result =~ "should be at least 12 character(s)"
+ assert result =~ "does not match password"
+ assert result =~ "is not valid"
+ end
+ end
+
+ describe "confirm email" do
+ setup %{conn: conn} do
+ user = user_fixture()
+ email = unique_user_email()
+
+ token =
+ extract_user_token(fn url ->
+ Accounts.deliver_user_update_email_instructions(%{user | email: email}, user.email, url)
+ end)
+
+ %{conn: log_in_user(conn, user), token: token, email: email, user: user}
+ end
+
+ test "updates the user email once", %{conn: conn, user: user, token: token, email: email} do
+ {:error, redirect} = live(conn, ~p"/users/settings/confirm_email/#{token}")
+
+ assert {:live_redirect, %{to: path, flash: flash}} = redirect
+ assert path == ~p"/users/settings"
+ assert %{"info" => message} = flash
+ assert message == "Email changed successfully."
+ refute Accounts.get_user_by_email(user.email)
+ assert Accounts.get_user_by_email(email)
+
+ # use confirm token again
+ {:error, redirect} = live(conn, ~p"/users/settings/confirm_email/#{token}")
+ assert {:live_redirect, %{to: path, flash: flash}} = redirect
+ assert path == ~p"/users/settings"
+ assert %{"error" => message} = flash
+ assert message == "Email change link is invalid or it has expired."
+ end
+
+ test "does not update email with invalid token", %{conn: conn, user: user} do
+ {:error, redirect} = live(conn, ~p"/users/settings/confirm_email/oops")
+ assert {:live_redirect, %{to: path, flash: flash}} = redirect
+ assert path == ~p"/users/settings"
+ assert %{"error" => message} = flash
+ assert message == "Email change link is invalid or it has expired."
+ assert Accounts.get_user_by_email(user.email)
+ end
+
+ test "redirects if user is not logged in", %{token: token} do
+ conn = build_conn()
+ {:error, redirect} = live(conn, ~p"/users/settings/confirm_email/#{token}")
+ assert {:redirect, %{to: path, flash: flash}} = redirect
+ assert path == ~p"/users/log_in"
+ assert %{"error" => message} = flash
+ assert message == "You must log in to access this page."
+ end
+ end
+end
diff --git a/Elixir/notes_app/test/notes_app_web/user_auth_test.exs b/Elixir/notes_app/test/notes_app_web/user_auth_test.exs
new file mode 100644
index 0000000..8318342
--- /dev/null
+++ b/Elixir/notes_app/test/notes_app_web/user_auth_test.exs
@@ -0,0 +1,272 @@
+defmodule NotesAppWeb.UserAuthTest do
+ use NotesAppWeb.ConnCase
+
+ alias Phoenix.LiveView
+ alias NotesApp.Accounts
+ alias NotesAppWeb.UserAuth
+ import NotesApp.AccountsFixtures
+
+ @remember_me_cookie "_notes_app_web_user_remember_me"
+
+ setup %{conn: conn} do
+ conn =
+ conn
+ |> Map.replace!(:secret_key_base, NotesAppWeb.Endpoint.config(:secret_key_base))
+ |> init_test_session(%{})
+
+ %{user: user_fixture(), conn: conn}
+ end
+
+ describe "log_in_user/3" do
+ test "stores the user token in the session", %{conn: conn, user: user} do
+ conn = UserAuth.log_in_user(conn, user)
+ assert token = get_session(conn, :user_token)
+ assert get_session(conn, :live_socket_id) == "users_sessions:#{Base.url_encode64(token)}"
+ assert redirected_to(conn) == ~p"/"
+ assert Accounts.get_user_by_session_token(token)
+ end
+
+ test "clears everything previously stored in the session", %{conn: conn, user: user} do
+ conn = conn |> put_session(:to_be_removed, "value") |> UserAuth.log_in_user(user)
+ refute get_session(conn, :to_be_removed)
+ end
+
+ test "redirects to the configured path", %{conn: conn, user: user} do
+ conn = conn |> put_session(:user_return_to, "/hello") |> UserAuth.log_in_user(user)
+ assert redirected_to(conn) == "/hello"
+ end
+
+ test "writes a cookie if remember_me is configured", %{conn: conn, user: user} do
+ conn = conn |> fetch_cookies() |> UserAuth.log_in_user(user, %{"remember_me" => "true"})
+ assert get_session(conn, :user_token) == conn.cookies[@remember_me_cookie]
+
+ assert %{value: signed_token, max_age: max_age} = conn.resp_cookies[@remember_me_cookie]
+ assert signed_token != get_session(conn, :user_token)
+ assert max_age == 5_184_000
+ end
+ end
+
+ describe "logout_user/1" do
+ test "erases session and cookies", %{conn: conn, user: user} do
+ user_token = Accounts.generate_user_session_token(user)
+
+ conn =
+ conn
+ |> put_session(:user_token, user_token)
+ |> put_req_cookie(@remember_me_cookie, user_token)
+ |> fetch_cookies()
+ |> UserAuth.log_out_user()
+
+ refute get_session(conn, :user_token)
+ refute conn.cookies[@remember_me_cookie]
+ assert %{max_age: 0} = conn.resp_cookies[@remember_me_cookie]
+ assert redirected_to(conn) == ~p"/"
+ refute Accounts.get_user_by_session_token(user_token)
+ end
+
+ test "broadcasts to the given live_socket_id", %{conn: conn} do
+ live_socket_id = "users_sessions:abcdef-token"
+ NotesAppWeb.Endpoint.subscribe(live_socket_id)
+
+ conn
+ |> put_session(:live_socket_id, live_socket_id)
+ |> UserAuth.log_out_user()
+
+ assert_receive %Phoenix.Socket.Broadcast{event: "disconnect", topic: ^live_socket_id}
+ end
+
+ test "works even if user is already logged out", %{conn: conn} do
+ conn = conn |> fetch_cookies() |> UserAuth.log_out_user()
+ refute get_session(conn, :user_token)
+ assert %{max_age: 0} = conn.resp_cookies[@remember_me_cookie]
+ assert redirected_to(conn) == ~p"/"
+ end
+ end
+
+ describe "fetch_current_user/2" do
+ test "authenticates user from session", %{conn: conn, user: user} do
+ user_token = Accounts.generate_user_session_token(user)
+ conn = conn |> put_session(:user_token, user_token) |> UserAuth.fetch_current_user([])
+ assert conn.assigns.current_user.id == user.id
+ end
+
+ test "authenticates user from cookies", %{conn: conn, user: user} do
+ logged_in_conn =
+ conn |> fetch_cookies() |> UserAuth.log_in_user(user, %{"remember_me" => "true"})
+
+ user_token = logged_in_conn.cookies[@remember_me_cookie]
+ %{value: signed_token} = logged_in_conn.resp_cookies[@remember_me_cookie]
+
+ conn =
+ conn
+ |> put_req_cookie(@remember_me_cookie, signed_token)
+ |> UserAuth.fetch_current_user([])
+
+ assert conn.assigns.current_user.id == user.id
+ assert get_session(conn, :user_token) == user_token
+
+ assert get_session(conn, :live_socket_id) ==
+ "users_sessions:#{Base.url_encode64(user_token)}"
+ end
+
+ test "does not authenticate if data is missing", %{conn: conn, user: user} do
+ _ = Accounts.generate_user_session_token(user)
+ conn = UserAuth.fetch_current_user(conn, [])
+ refute get_session(conn, :user_token)
+ refute conn.assigns.current_user
+ end
+ end
+
+ describe "on_mount :mount_current_user" do
+ test "assigns current_user based on a valid user_token", %{conn: conn, user: user} do
+ user_token = Accounts.generate_user_session_token(user)
+ session = conn |> put_session(:user_token, user_token) |> get_session()
+
+ {:cont, updated_socket} =
+ UserAuth.on_mount(:mount_current_user, %{}, session, %LiveView.Socket{})
+
+ assert updated_socket.assigns.current_user.id == user.id
+ end
+
+ test "assigns nil to current_user assign if there isn't a valid user_token", %{conn: conn} do
+ user_token = "invalid_token"
+ session = conn |> put_session(:user_token, user_token) |> get_session()
+
+ {:cont, updated_socket} =
+ UserAuth.on_mount(:mount_current_user, %{}, session, %LiveView.Socket{})
+
+ assert updated_socket.assigns.current_user == nil
+ end
+
+ test "assigns nil to current_user assign if there isn't a user_token", %{conn: conn} do
+ session = conn |> get_session()
+
+ {:cont, updated_socket} =
+ UserAuth.on_mount(:mount_current_user, %{}, session, %LiveView.Socket{})
+
+ assert updated_socket.assigns.current_user == nil
+ end
+ end
+
+ describe "on_mount :ensure_authenticated" do
+ test "authenticates current_user based on a valid user_token", %{conn: conn, user: user} do
+ user_token = Accounts.generate_user_session_token(user)
+ session = conn |> put_session(:user_token, user_token) |> get_session()
+
+ {:cont, updated_socket} =
+ UserAuth.on_mount(:ensure_authenticated, %{}, session, %LiveView.Socket{})
+
+ assert updated_socket.assigns.current_user.id == user.id
+ end
+
+ test "redirects to login page if there isn't a valid user_token", %{conn: conn} do
+ user_token = "invalid_token"
+ session = conn |> put_session(:user_token, user_token) |> get_session()
+
+ socket = %LiveView.Socket{
+ endpoint: NotesAppWeb.Endpoint,
+ assigns: %{__changed__: %{}, flash: %{}}
+ }
+
+ {:halt, updated_socket} = UserAuth.on_mount(:ensure_authenticated, %{}, session, socket)
+ assert updated_socket.assigns.current_user == nil
+ end
+
+ test "redirects to login page if there isn't a user_token", %{conn: conn} do
+ session = conn |> get_session()
+
+ socket = %LiveView.Socket{
+ endpoint: NotesAppWeb.Endpoint,
+ assigns: %{__changed__: %{}, flash: %{}}
+ }
+
+ {:halt, updated_socket} = UserAuth.on_mount(:ensure_authenticated, %{}, session, socket)
+ assert updated_socket.assigns.current_user == nil
+ end
+ end
+
+ describe "on_mount :redirect_if_user_is_authenticated" do
+ test "redirects if there is an authenticated user ", %{conn: conn, user: user} do
+ user_token = Accounts.generate_user_session_token(user)
+ session = conn |> put_session(:user_token, user_token) |> get_session()
+
+ assert {:halt, _updated_socket} =
+ UserAuth.on_mount(
+ :redirect_if_user_is_authenticated,
+ %{},
+ session,
+ %LiveView.Socket{}
+ )
+ end
+
+ test "doesn't redirect if there is no authenticated user", %{conn: conn} do
+ session = conn |> get_session()
+
+ assert {:cont, _updated_socket} =
+ UserAuth.on_mount(
+ :redirect_if_user_is_authenticated,
+ %{},
+ session,
+ %LiveView.Socket{}
+ )
+ end
+ end
+
+ describe "redirect_if_user_is_authenticated/2" do
+ test "redirects if user is authenticated", %{conn: conn, user: user} do
+ conn = conn |> assign(:current_user, user) |> UserAuth.redirect_if_user_is_authenticated([])
+ assert conn.halted
+ assert redirected_to(conn) == ~p"/"
+ end
+
+ test "does not redirect if user is not authenticated", %{conn: conn} do
+ conn = UserAuth.redirect_if_user_is_authenticated(conn, [])
+ refute conn.halted
+ refute conn.status
+ end
+ end
+
+ describe "require_authenticated_user/2" do
+ test "redirects if user is not authenticated", %{conn: conn} do
+ conn = conn |> fetch_flash() |> UserAuth.require_authenticated_user([])
+ assert conn.halted
+
+ assert redirected_to(conn) == ~p"/users/log_in"
+
+ assert Phoenix.Flash.get(conn.assigns.flash, :error) ==
+ "You must log in to access this page."
+ end
+
+ test "stores the path to redirect to on GET", %{conn: conn} do
+ halted_conn =
+ %{conn | path_info: ["foo"], query_string: ""}
+ |> fetch_flash()
+ |> UserAuth.require_authenticated_user([])
+
+ assert halted_conn.halted
+ assert get_session(halted_conn, :user_return_to) == "/foo"
+
+ halted_conn =
+ %{conn | path_info: ["foo"], query_string: "bar=baz"}
+ |> fetch_flash()
+ |> UserAuth.require_authenticated_user([])
+
+ assert halted_conn.halted
+ assert get_session(halted_conn, :user_return_to) == "/foo?bar=baz"
+
+ halted_conn =
+ %{conn | path_info: ["foo"], query_string: "bar", method: "POST"}
+ |> fetch_flash()
+ |> UserAuth.require_authenticated_user([])
+
+ assert halted_conn.halted
+ refute get_session(halted_conn, :user_return_to)
+ end
+
+ test "does not redirect if user is authenticated", %{conn: conn, user: user} do
+ conn = conn |> assign(:current_user, user) |> UserAuth.require_authenticated_user([])
+ refute conn.halted
+ refute conn.status
+ end
+ end
+end
diff --git a/Elixir/notes_app/test/support/conn_case.ex b/Elixir/notes_app/test/support/conn_case.ex
new file mode 100644
index 0000000..f95ec02
--- /dev/null
+++ b/Elixir/notes_app/test/support/conn_case.ex
@@ -0,0 +1,64 @@
+defmodule NotesAppWeb.ConnCase do
+ @moduledoc """
+ This module defines the test case to be used by
+ tests that require setting up a connection.
+
+ Such tests rely on `Phoenix.ConnTest` and also
+ import other functionality to make it easier
+ to build common data structures and query the data layer.
+
+ Finally, if the test case interacts with the database,
+ we enable the SQL sandbox, so changes done to the database
+ are reverted at the end of every test. If you are using
+ PostgreSQL, you can even run database tests asynchronously
+ by setting `use NotesAppWeb.ConnCase, async: true`, although
+ this option is not recommended for other databases.
+ """
+
+ use ExUnit.CaseTemplate
+
+ using do
+ quote do
+ # The default endpoint for testing
+ @endpoint NotesAppWeb.Endpoint
+
+ use NotesAppWeb, :verified_routes
+
+ # Import conveniences for testing with connections
+ import Plug.Conn
+ import Phoenix.ConnTest
+ import NotesAppWeb.ConnCase
+ end
+ end
+
+ setup tags do
+ NotesApp.DataCase.setup_sandbox(tags)
+ {:ok, conn: Phoenix.ConnTest.build_conn()}
+ end
+
+ @doc """
+ Setup helper that registers and logs in users.
+
+ setup :register_and_log_in_user
+
+ It stores an updated connection and a registered user in the
+ test context.
+ """
+ def register_and_log_in_user(%{conn: conn}) do
+ user = NotesApp.AccountsFixtures.user_fixture()
+ %{conn: log_in_user(conn, user), user: user}
+ end
+
+ @doc """
+ Logs the given `user` into the `conn`.
+
+ It returns an updated `conn`.
+ """
+ def log_in_user(conn, user) do
+ token = NotesApp.Accounts.generate_user_session_token(user)
+
+ conn
+ |> Phoenix.ConnTest.init_test_session(%{})
+ |> Plug.Conn.put_session(:user_token, token)
+ end
+end
diff --git a/Elixir/notes_app/test/support/data_case.ex b/Elixir/notes_app/test/support/data_case.ex
new file mode 100644
index 0000000..9c155d2
--- /dev/null
+++ b/Elixir/notes_app/test/support/data_case.ex
@@ -0,0 +1,58 @@
+defmodule NotesApp.DataCase do
+ @moduledoc """
+ This module defines the setup for tests requiring
+ access to the application's data layer.
+
+ You may define functions here to be used as helpers in
+ your tests.
+
+ Finally, if the test case interacts with the database,
+ we enable the SQL sandbox, so changes done to the database
+ are reverted at the end of every test. If you are using
+ PostgreSQL, you can even run database tests asynchronously
+ by setting `use NotesApp.DataCase, async: true`, although
+ this option is not recommended for other databases.
+ """
+
+ use ExUnit.CaseTemplate
+
+ using do
+ quote do
+ alias NotesApp.Repo
+
+ import Ecto
+ import Ecto.Changeset
+ import Ecto.Query
+ import NotesApp.DataCase
+ end
+ end
+
+ setup tags do
+ NotesApp.DataCase.setup_sandbox(tags)
+ :ok
+ end
+
+ @doc """
+ Sets up the sandbox based on the test tags.
+ """
+ def setup_sandbox(tags) do
+ pid = Ecto.Adapters.SQL.Sandbox.start_owner!(NotesApp.Repo, shared: not tags[:async])
+ on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
+ end
+
+ @doc """
+ A helper that transforms changeset errors into a map of messages.
+
+ assert {:error, changeset} = Accounts.create_user(%{password: "short"})
+ assert "password is too short" in errors_on(changeset).password
+ assert %{password: ["password is too short"]} = errors_on(changeset)
+
+ """
+ def errors_on(changeset) do
+ Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
+ Regex.replace(~r"%{(\w+)}", message, fn _, key ->
+ opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
+ end)
+ end)
+ end
+end
diff --git a/Elixir/notes_app/test/support/fixtures/accounts_fixtures.ex b/Elixir/notes_app/test/support/fixtures/accounts_fixtures.ex
new file mode 100644
index 0000000..627033e
--- /dev/null
+++ b/Elixir/notes_app/test/support/fixtures/accounts_fixtures.ex
@@ -0,0 +1,31 @@
+defmodule NotesApp.AccountsFixtures do
+ @moduledoc """
+ This module defines test helpers for creating
+ entities via the `NotesApp.Accounts` context.
+ """
+
+ def unique_user_email, do: "user#{System.unique_integer()}@example.com"
+ def valid_user_password, do: "hello world!"
+
+ def valid_user_attributes(attrs \\ %{}) do
+ Enum.into(attrs, %{
+ email: unique_user_email(),
+ password: valid_user_password()
+ })
+ end
+
+ def user_fixture(attrs \\ %{}) do
+ {:ok, user} =
+ attrs
+ |> valid_user_attributes()
+ |> NotesApp.Accounts.register_user()
+
+ user
+ end
+
+ def extract_user_token(fun) do
+ {:ok, captured_email} = fun.(&"[TOKEN]#{&1}[TOKEN]")
+ [_, token | _] = String.split(captured_email.text_body, "[TOKEN]")
+ token
+ end
+end
diff --git a/Elixir/notes_app/test/test_helper.exs b/Elixir/notes_app/test/test_helper.exs
new file mode 100644
index 0000000..d3bc9c3
--- /dev/null
+++ b/Elixir/notes_app/test/test_helper.exs
@@ -0,0 +1,2 @@
+ExUnit.start()
+Ecto.Adapters.SQL.Sandbox.mode(NotesApp.Repo, :manual)