├── .gitignore ├── .tool-versions ├── Dockerfile ├── README.md ├── circle.yml ├── config └── config.exs ├── docker-compose.yml ├── lib ├── cron.ex └── cron │ ├── api.ex │ ├── models │ └── event.ex │ ├── repo.ex │ ├── routes │ ├── event.ex │ └── healthz.ex │ └── services │ ├── request.ex │ └── scheduler.ex ├── mix.exs ├── mix.lock ├── priv └── repo │ └── migrations │ └── 20161113151412_create_events.exs └── test ├── cron ├── event_test.exs ├── routes_test.exs └── scheduler_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | /rel 5 | erl_crash.dump 6 | *.ez 7 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | erlang 18.0 2 | elixir 1.2.5 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM aweiker/alpine-elixir 2 | 3 | ENV APP_NAME cron 4 | 5 | COPY . /source 6 | WORKDIR /source 7 | 8 | RUN mix local.hex --force && mix local.rebar --force 9 | RUN MIX_ENV=prod mix deps.get 10 | RUN MIX_ENV=prod mix compile 11 | RUN MIX_ENV=prod mix release --verbosity=verbose --no-confirm-missing 12 | RUN mkdir /app && cp -r rel/$APP_NAME /app && rm -rf /source 13 | 14 | CMD trap exit TERM; /app/$APP_NAME/bin/$APP_NAME foreground & wait 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CronApi 2 | 3 | [![CircleCI](https://circleci.com/gh/rafaeljesus/cron_api.svg?style=svg)](https://circleci.com/gh/rafaeljesus/cron_api) 4 | [![Deps Status](https://beta.hexfaktor.org/badge/all/github/rafaeljesus/cron_api.svg)](https://beta.hexfaktor.org/github/rafaeljesus/cron_api) 5 | 6 | * Cron-like job scheduler as a service 7 | * A minimal docker alpine container 8 | * Automatically pushes it to dockerhub if tests pass 9 | 10 | ## Installation 11 | ```bash 12 | git clone https://github.com/rafaeljesus/cron_api.git 13 | cd cron_api 14 | mix deps.get 15 | ``` 16 | 17 | ## Running server 18 | To start the serve execute: 19 | ```bash 20 | iex -S mix 21 | ``` 22 | 23 | ## API Documentation 24 | ### create 25 | ```bash 26 | curl -X POST 'http://localhost:3000/v1/events' \ 27 | -d 'url=https://api.your-server.com/your-url' \ 28 | -d 'cron=* * * * *' \ 29 | -d 'status=active' 30 | ``` 31 | 32 | ### update 33 | ```bash 34 | curl -X PATCH 'http://localhost:3000/v1/events/1' \ 35 | -d 'cron=1 * * * *' \ 36 | -d 'status=inactive' 37 | ``` 38 | 39 | ### show 40 | ```bash 41 | curl -X GET 'http://localhost:3000/v1/events/1' 42 | ``` 43 | 44 | ### delete 45 | ```bash 46 | curl -X DELETE 'http://localhost:3000/v1/events/1' 47 | ``` 48 | 49 | ### search 50 | ```bash 51 | curl -X GET 'http://localhost:3000/v1/events/?status=active&page=2&page_size=25' 52 | ``` 53 | 54 | ## Built with 55 | - [elixir](http://elixir-lang.org) Backend is a elixir 1.2. 56 | - [maru](https://github.com/falood/maru) API is exposed by maru. HTTP microservices 57 | - [Mongodb](https://www.mongodb.com) Mongodb as a data store. 58 | 59 | ## Docker 60 | This repository has automated image builds on hub.docker.com after successfully building and testing. See the `deployment` section of [circle.yml](circle.yml) for details on how this is done. Note that three environment variables need to be set on CircleCI for the deployment to work: 61 | 62 | * DOCKER_EMAIL - The email address associated with the user with push access to the Docker Hub repository 63 | * DOCKER_USER - Docker Hub username 64 | * DOCKER_PASS - Docker Hub password (these are all stored encrypted on CircleCI, and you can create a deployment user with limited permission on Docker Hub if you like) 65 | 66 | run: 67 | ``` 68 | $ docker-machine start default 69 | $ eval $(docker-machine env default) 70 | $ docker-compose up 71 | $ curl `docker-machine ip default`:3000 72 | ``` 73 | 74 | ## Contributing 75 | - Fork it 76 | - Create your feature branch (`git checkout -b my-new-feature`) 77 | - Commit your changes (`git commit -am 'Add some feature'`) 78 | - Push to the branch (`git push origin my-new-feature`) 79 | - Create new Pull Request 80 | 81 | ### Maintaners 82 | 83 | * [Rafael Jesus](https://github.com/rafaeljesus) 84 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | pre: 3 | - curl -sSL https://s3.amazonaws.com/circle-downloads/install-circleci-docker.sh | bash -s -- 1.10.0 4 | environment: 5 | PATH: "$HOME/.asdf/bin:$HOME/.asdf/shims:$PATH" 6 | services: 7 | - docker 8 | 9 | dependencies: 10 | pre: 11 | - if ! asdf | grep version; then git clone https://github.com/HashNuke/asdf.git ~/.asdf; fi 12 | - if ! asdf plugin-list | grep erlang; then asdf plugin-add erlang https://github.com/HashNuke/asdf-erlang.git; fi 13 | - if ! asdf plugin-list | grep elixir; then asdf plugin-add elixir https://github.com/HashNuke/asdf-elixir.git; fi 14 | - erlang_version=$(awk '/erlang/ { print $2 }' .tool-versions) && asdf install erlang ${erlang_version} 15 | - elixir_version=$(awk '/elixir/ { print $2 }' .tool-versions) && asdf install elixir ${elixir_version} 16 | - mix local.rebar --force 17 | - yes | mix deps.get 18 | cache_directories: 19 | - ~/.asdf 20 | 21 | deployment: 22 | master: 23 | branch: master 24 | commands: 25 | - docker build -t rafaeljesus/cron_api . 26 | - docker login -e $DOCKERHUB_EMAIL -u $DOCKERHUB_USER -p $DOCKERHUB_PASS 27 | - docker tag rafaeljesus/cron_api rafaeljesus/cron_api:master 28 | - docker push rafaeljesus/cron_api:master 29 | 30 | test: 31 | override: 32 | - mix test 33 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :maru, Cron.API, 4 | versioning: [ 5 | using: :path 6 | ], 7 | http: [port: 3000] 8 | 9 | config :logger, 10 | backends: [:console], 11 | format: "$time $metadata[$level] $message\n", 12 | metadata: [:request_id] 13 | 14 | config :cron, ecto_repos: [Cron.Repo] 15 | 16 | config :cron, Cron.Repo, 17 | adapter: Ecto.Adapters.Postgres, 18 | url: {:system, "CRON_API_URL"} 19 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | api: 4 | build: . 5 | environment: 6 | MONGO_PORT_27017_TCP_ADDR: "0.0.0.0" 7 | command: iex -S mix 8 | ports: 9 | - "3000:3000" 10 | volumes: 11 | - .:/cron_api 12 | - .:/cron_api/deps 13 | links: 14 | - db 15 | db: 16 | image: mongo 17 | ports: 18 | - "27017:27017" 19 | volumes: 20 | - /data/db2/:/data/db/ 21 | command: /usr/bin/mongod --smallfiles 22 | -------------------------------------------------------------------------------- /lib/cron.ex: -------------------------------------------------------------------------------- 1 | defmodule Cron do 2 | use Application 3 | 4 | import Supervisor.Spec 5 | 6 | def start(_type, _args), 7 | do: Supervisor.start_link(children, opts) 8 | 9 | defp opts, do: [strategy: :one_for_one, name: Cron] 10 | 11 | defp children do 12 | [supervisor(Cron.Repo, [])] 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/cron/api.ex: -------------------------------------------------------------------------------- 1 | defmodule Cron.API do 2 | use Maru.Router 3 | 4 | plug CORSPlug 5 | plug Plug.Logger 6 | plug Plug.Parsers, 7 | pass: ["*/*"], 8 | parsers: [:json], 9 | json_decoder: Poison 10 | 11 | mount Cron.Routes.Healthz 12 | mount Cron.Routes.Event 13 | 14 | rescue_from :all, as: e do 15 | IO.inspect(e) 16 | conn 17 | |> put_status(500) 18 | |> json(%{message: "Server Error"}) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/cron/models/event.ex: -------------------------------------------------------------------------------- 1 | defmodule Cron.Models.Event do 2 | use Ecto.Schema 3 | 4 | import Ecto.Changeset 5 | 6 | alias Cron.Repo 7 | alias Cron.Models.Event 8 | 9 | @required_fields ~w(url cron) 10 | @optional_fields ~w(status) 11 | 12 | @derive {Poison.Encoder, except: [:__meta__, :__struct__]} 13 | schema "events" do 14 | field :url, :string 15 | field :cron, :string 16 | field :status, :string, default: "active" 17 | 18 | timestamps 19 | end 20 | 21 | def changeset(event, params \\ %{}) do 22 | event 23 | |> cast(params, @required_fields, @optional_fields) 24 | |> validate_required([:url, :cron]) 25 | end 26 | 27 | def search(params \\ %{status: "active"}) do 28 | status = params[:status] 29 | Event 30 | |> where([c], c.status == ^status) 31 | |> Repo.paginate(params) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/cron/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule Cron.Repo do 2 | use Ecto.Repo, otp_app: :cron 3 | end 4 | -------------------------------------------------------------------------------- /lib/cron/routes/event.ex: -------------------------------------------------------------------------------- 1 | defmodule Cron.Routes.Event do 2 | use Maru.Router 3 | 4 | alias Cron.Repo 5 | alias Cron.Models.Event 6 | alias Cron.Services.Scheduler 7 | 8 | version "v1" 9 | 10 | namespace :events do 11 | get do 12 | result = conn 13 | |> fetch_query_params 14 | |> Map.get(:params) 15 | |> Event.search 16 | 17 | json(conn, result) 18 | end 19 | 20 | params do 21 | requires :url, type: String 22 | requires :cron, type: String 23 | optional :status, type: String 24 | end 25 | post do 26 | %Event{} 27 | |> Event.changeset(params) 28 | |> Repo.insert 29 | |> Scheduler.create 30 | 31 | conn 32 | |> put_status(:created) 33 | |> put_resp_content_type("application/json") 34 | end 35 | 36 | route_param :id do 37 | get do 38 | event = Repo.get!(Event, params[:id]) 39 | json(conn, event) 40 | end 41 | 42 | params do 43 | optional :url, type: String 44 | optional :cron, type: String 45 | optional :status, type: String 46 | at_least_one_of [:url, :cron, :status] 47 | end 48 | patch do 49 | event = Event 50 | |> Repo.get(params[:id]) 51 | |> Event.changeset(params) 52 | |> Repo.update 53 | |> Scheduler.update 54 | 55 | json(conn, event) 56 | end 57 | 58 | delete do 59 | Event 60 | |> Repo.get(params[:id]) 61 | |> Repo.delete 62 | |> Scheduler.delete 63 | 64 | conn 65 | |> put_status(204) 66 | |> put_resp_content_type("application/json") 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/cron/routes/healthz.ex: -------------------------------------------------------------------------------- 1 | defmodule Cron.Routes.Healthz do 2 | use Maru.Router 3 | 4 | version "v1" 5 | 6 | namespace :healthz do 7 | get do 8 | json(conn, %{alive: true}) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/cron/services/request.ex: -------------------------------------------------------------------------------- 1 | defmodule Cron.Services.Request do 2 | def call(url) do 3 | url 4 | |> HTTPoison.get 5 | |> handle 6 | end 7 | 8 | defp handle({:ok, %{status_code: 200} = response}), do: {:ok, response} 9 | defp handle({:error, error}), do: {:error, :unfetchable} 10 | end 11 | -------------------------------------------------------------------------------- /lib/cron/services/scheduler.ex: -------------------------------------------------------------------------------- 1 | defmodule Cron.Services.Scheduler do 2 | require Logger 3 | 4 | alias Quantum 5 | alias Cron.Services.Request 6 | 7 | def create(event) do 8 | job = %Quantum.Job{ 9 | schedule: event.cron, 10 | task: fn -> Request.call(event.url) end 11 | } 12 | 13 | event.id 14 | |> encode_id 15 | |> Quantum.add_job(job) 16 | end 17 | 18 | def find(id) do 19 | id 20 | |> encode_id 21 | |> Quantum.find_job 22 | end 23 | 24 | def update(event) do 25 | delete(event.id) 26 | create(event) 27 | end 28 | 29 | def delete(id) do 30 | id 31 | |> encode_id 32 | |> Quantum.delete_job 33 | end 34 | 35 | defp encode_id(id) do 36 | str = "event_" <> Integer.to_string(id) 37 | String.to_atom("event_" <> str) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Cron.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :cron, 6 | version: "0.0.1", 7 | elixir: "~> 1.2", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | description: description, 11 | package: package, 12 | deps: deps] 13 | end 14 | 15 | # Configuration for the OTP application 16 | # 17 | # Type "mix help compile.app" for more information 18 | def application do 19 | [applications: [:logger, :maru, :postgrex, :ecto, :scrivener_ecto, 20 | :httpoison, :quantum], 21 | mod: {Cron, []} 22 | ] 23 | end 24 | 25 | # Dependencies can be Hex packages: 26 | # 27 | # {:mydep, "~> 0.3.0"} 28 | # 29 | # Or git/path repositories: 30 | # 31 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} 32 | # 33 | # Type "mix help deps" for more examples and options 34 | defp deps do 35 | [ 36 | {:maru, "~> 0.10.5"}, 37 | {:poison, "~> 3.0", override: true}, 38 | {:cors_plug, "~> 1.1.1"}, 39 | {:postgrex, "~> 0.12.1"}, 40 | {:ecto, "~> 2.0.5"}, 41 | {:scrivener_ecto, "~> 1.0"}, 42 | {:exrm, "~> 1.0.5"}, 43 | {:quantum, ">= 1.8.0"}, 44 | {:httpoison, "~> 0.10.0"}, 45 | {:dogma, "~> 0.1", only: [:dev, :test]}, 46 | ] 47 | end 48 | 49 | defp description do 50 | """ 51 | Cron-like job scheduler as a service 52 | """ 53 | end 54 | 55 | defp package do 56 | [maintainers: ["Rafael Jesus"], 57 | licenses: ["Apache 2.0"], 58 | links: %{"GitHub" => "https://github.com/rafaeljesus/cron_api"}, 59 | files: ~w(mix.exs README.md lib)] 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"bbmustache": {:hex, :bbmustache, "1.0.4", "7ba94f971c5afd7b6617918a4bb74705e36cab36eb84b19b6a1b7ee06427aa38", [:rebar], []}, 2 | "certifi": {:hex, :certifi, "0.7.0", "861a57f3808f7eb0c2d1802afeaae0fa5de813b0df0979153cbafcd853ababaf", [:rebar3], []}, 3 | "cf": {:hex, :cf, "0.2.1", "69d0b1349fd4d7d4dc55b7f407d29d7a840bf9a1ef5af529f1ebe0ce153fc2ab", [:rebar3], []}, 4 | "combine": {:hex, :combine, "0.9.3", "192e609b48b3f2210494e26f85db1712657be1a8f15795656710317ea43fc449", [:mix], []}, 5 | "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], []}, 6 | "cors_plug": {:hex, :cors_plug, "1.1.2", "3e7451286996f745c7b629c39d24a6493e59b0c8191f27e67f6ab097f96ffd23", [:mix], [{:cowboy, "~> 1.0.0", [hex: :cowboy, optional: false]}, {:plug, "> 0.8.0", [hex: :plug, optional: false]}]}, 7 | "cowboy": {:hex, :cowboy, "1.0.4", "a324a8df9f2316c833a470d918aaf73ae894278b8aa6226ce7a9bf699388f878", [:make, :rebar], [{:cowlib, "~> 1.0.0", [hex: :cowlib, optional: false]}, {:ranch, "~> 1.0", [hex: :ranch, optional: false]}]}, 8 | "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], []}, 9 | "db_connection": {:hex, :db_connection, "1.1.0", "b2b88db6d7d12f99997b584d09fad98e560b817a20dab6a526830e339f54cdb3", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, optional: true]}]}, 10 | "decimal": {:hex, :decimal, "1.3.1", "157b3cedb2bfcb5359372a7766dd7a41091ad34578296e951f58a946fcab49c6", [:mix], []}, 11 | "dogma": {:hex, :dogma, "0.1.13", "7b6c6ad2b3ee6501eda3bd39e197dd5198be8d520d1c175c7f713803683cf27a", [:mix], [{:poison, ">= 2.0.0", [hex: :poison, optional: false]}]}, 12 | "ecto": {:hex, :ecto, "2.0.5", "7f4c79ac41ffba1a4c032b69d7045489f0069c256de606523c65d9f8188e502d", [:mix], [{:db_connection, "~> 1.0-rc.4", [hex: :db_connection, optional: true]}, {:decimal, "~> 1.1.2 or ~> 1.2", [hex: :decimal, optional: false]}, {:mariaex, "~> 0.7.7", [hex: :mariaex, optional: true]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: false]}, {:postgrex, "~> 0.12.0", [hex: :postgrex, optional: true]}, {:sbroker, "~> 1.0-beta", [hex: :sbroker, optional: true]}]}, 13 | "erlware_commons": {:hex, :erlware_commons, "0.21.0", "a04433071ad7d112edefc75ac77719dd3e6753e697ac09428fc83d7564b80b15", [:rebar3], [{:cf, "0.2.1", [hex: :cf, optional: false]}]}, 14 | "exrm": {:hex, :exrm, "1.0.8", "5aa8990cdfe300282828b02cefdc339e235f7916388ce99f9a1f926a9271a45d", [:mix], [{:relx, "~> 3.5", [hex: :relx, optional: false]}]}, 15 | "getopt": {:hex, :getopt, "0.8.2", "b17556db683000ba50370b16c0619df1337e7af7ecbf7d64fbf8d1d6bce3109b", [:rebar], []}, 16 | "gettext": {:hex, :gettext, "0.12.1", "c0624f52763469ef7a3674919ae28b8286d88195b90fa1516180f31bbbd26d14", [:mix], []}, 17 | "hackney": {:hex, :hackney, "1.6.3", "d489d7ca2d4323e307bedc4bfe684323a7bf773ecfd77938f3ee8074e488e140", [:mix, :rebar3], [{:certifi, "0.7.0", [hex: :certifi, optional: false]}, {:idna, "1.2.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, optional: false]}]}, 18 | "httpoison": {:hex, :httpoison, "0.10.0", "4727b3a5e57e9a4ff168a3c2883e20f1208103a41bccc4754f15a9366f49b676", [:mix], [{:hackney, "~> 1.6.3", [hex: :hackney, optional: false]}]}, 19 | "idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], []}, 20 | "maru": {:hex, :maru, "0.10.5", "08f5c1f1279e22ab6b92eb24a509a5329d8eccce333d571396d0175175c095b8", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: false]}, {:plug, "~> 1.0", [hex: :plug, optional: false]}, {:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, optional: false]}]}, 21 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, 22 | "mime": {:hex, :mime, "1.0.1", "05c393850524767d13a53627df71beeebb016205eb43bfbd92d14d24ec7a1b51", [:mix], []}, 23 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, 24 | "plug": {:hex, :plug, "1.2.2", "cfbda521b54c92ab8ddffb173fbaabed8d8fc94bec07cd9bb58a84c1c501b0bd", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: true]}, {:mime, "~> 1.0", [hex: :mime, optional: false]}]}, 25 | "poison": {:hex, :poison, "3.0.0", "625ebd64d33ae2e65201c2c14d6c85c27cc8b68f2d0dd37828fde9c6920dd131", [:mix], []}, 26 | "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], []}, 27 | "postgrex": {:hex, :postgrex, "0.12.1", "2f8b46cb3a44dcd42f42938abedbfffe7e103ba4ce810ccbeee8dcf27ca0fb06", [:mix], [{:connection, "~> 1.0", [hex: :connection, optional: false]}, {:db_connection, "~> 1.0-rc.4", [hex: :db_connection, optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, optional: false]}]}, 28 | "providers": {:hex, :providers, "1.6.0", "db0e2f9043ae60c0155205fcd238d68516331d0e5146155e33d1e79dc452964a", [:rebar3], [{:getopt, "0.8.2", [hex: :getopt, optional: false]}]}, 29 | "quantum": {:hex, :quantum, "1.8.0", "1f7a049b5ac47bd138a54fcd54788f886d656c90c5be29df91bc06f5def4068e", [:mix], [{:timex, "~> 3.0", [hex: :timex, optional: false]}]}, 30 | "ranch": {:hex, :ranch, "1.2.1", "a6fb992c10f2187b46ffd17ce398ddf8a54f691b81768f9ef5f461ea7e28c762", [:make], []}, 31 | "relx": {:hex, :relx, "3.21.1", "f989dc520730efd9075e9f4debcb8ba1d7d1e86b018b0bcf45a2eb80270b4ad6", [:rebar3], [{:bbmustache, "1.0.4", [hex: :bbmustache, optional: false]}, {:cf, "0.2.1", [hex: :cf, optional: false]}, {:erlware_commons, "0.21.0", [hex: :erlware_commons, optional: false]}, {:getopt, "0.8.2", [hex: :getopt, optional: false]}, {:providers, "1.6.0", [hex: :providers, optional: false]}]}, 32 | "scrivener": {:hex, :scrivener, "2.1.1", "eb52c8b7d283e8999edd6fd50d872ab870669d1f4504134841d0845af11b5ef3", [:mix], []}, 33 | "scrivener_ecto": {:hex, :scrivener_ecto, "1.0.2", "4b10a2e6c23ed8aae59731d7ae71bfd55afea6559aae61b124e6e521055b4a9c", [:mix], [{:ecto, "~> 2.0", [hex: :ecto, optional: false]}, {:postgrex, "~> 0.11.0 or ~> 0.12.0", [hex: :postgrex, optional: true]}, {:scrivener, "~> 2.0", [hex: :scrivener, optional: false]}]}, 34 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], []}, 35 | "timex": {:hex, :timex, "3.1.5", "413d6d8d6f0162a5d47080cb8ca520d790184ac43e097c95191c7563bf25b428", [:mix], [{:combine, "~> 0.7", [hex: :combine, optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, optional: false]}]}, 36 | "tzdata": {:hex, :tzdata, "0.5.9", "575be217b039057a47e133b72838cbe104fb5329b19906ea4e66857001c37edb", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, optional: false]}]}} 37 | -------------------------------------------------------------------------------- /priv/repo/migrations/20161113151412_create_events.exs: -------------------------------------------------------------------------------- 1 | defmodule Cron.Repo.Migrations.CreateEvents do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:events) do 6 | add :url, :string, null: false 7 | add :cron, :string, null: false 8 | add :status, :string 9 | 10 | timestamps 11 | end 12 | 13 | create index(:events, [:url]) 14 | create index(:events, [:cron]) 15 | create index(:events, [:status]) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/cron/event_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Cron.Models.EventTest do 2 | use ExUnit.Case 3 | 4 | alias Cron.Models.Event 5 | 6 | @invalid_attrs %{} 7 | @valid_attrs %{ 8 | url: "https://api.github.com/users/rafaeljesus/events", 9 | cron: "* * * * *", 10 | status: "active" 11 | } 12 | 13 | test "changeset with valid attributes" do 14 | changeset = Event.changeset(%Event{}, @valid_attrs) 15 | assert changeset.valid? 16 | end 17 | 18 | test "changeset with invalid attributes" do 19 | changeset = Event.changeset(%Event{}, @invalid_attrs) 20 | refute changeset.valid? 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/cron/routes_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Cron.Routes.EventTest do 2 | use ExUnit.Case 3 | use Maru.Test, for: Cron.API 4 | 5 | alias Cron.Repo 6 | alias Cron.Models.Event 7 | 8 | @valid_attrs %{ 9 | url: "https://api.github.com/users/rafaeljesus/events", 10 | cron: "* * * * *", 11 | status: "active" 12 | } 13 | 14 | setup do 15 | model = %Event{} 16 | |> Event.changeset(@valid_attrs) 17 | |> Repo.insert! 18 | 19 | on_exit fn -> 20 | Repo.delete_all(Event) 21 | end 22 | 23 | {:ok, %{model: model}} 24 | end 25 | 26 | test "POST /v1/events" do 27 | conn = conn(:post, "/v1/events", @valid_attrs) 28 | |> put_req_header("content-type", "application/json") 29 | |> make_response 30 | 31 | assert conn.state == :sent 32 | assert conn.status == 200 33 | end 34 | 35 | test "PATCH /v1/events/:id", %{model: model} do 36 | conn = conn(:patch, "/v1/events/#{model.id}", %{status: 'inactive'}) 37 | |> put_req_header("content-type", "application/json") 38 | |> make_response 39 | 40 | assert conn.state == :sent 41 | assert conn.status == 200 42 | end 43 | 44 | test "GET /v1/events/:id", %{model: model} do 45 | conn = conn(:get, "/v1/events/#{model.id}") 46 | |> put_req_header("content-type", "application/json") 47 | |> make_response 48 | assert conn.status == 200 49 | end 50 | 51 | test "GET /v1/events" do 52 | conn = conn(:get, "/v1/events") 53 | |> put_req_header("content-type", "application/json") 54 | |> make_response 55 | assert conn.state == :sent 56 | assert conn.status == 200 57 | end 58 | 59 | test "DELETE /v1/events/:id", %{model: model} do 60 | conn = conn(:delete, "/v1/events/#{model.id}") 61 | |> put_req_header("content-type", "application/json") 62 | |> make_response 63 | assert conn.status == 200 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /test/cron/scheduler_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Cron.Services.SchedulerTest do 2 | use ExUnit.Case 3 | 4 | alias Cron.Models.Event 5 | alias Cron.Services.Scheduler 6 | 7 | @id 1 8 | @cron "* * * * *" 9 | 10 | setup do 11 | on_exit fn -> Scheduler.delete(@id) end 12 | event = %Event{id: @id, cron: @cron} 13 | {:ok, %{event: event}} 14 | end 15 | 16 | test "creates an job", %{event: event} do 17 | job_id = Scheduler.create(event) 18 | assert job_id != nil 19 | end 20 | 21 | test "finds an job", %{event: event} do 22 | Scheduler.create(event) 23 | job = Scheduler.find(event.id) 24 | assert job.state == :active 25 | end 26 | 27 | test "deletes an job", %{event: event} do 28 | Scheduler.delete(event.id) 29 | job = Scheduler.find(event.id) 30 | assert job == nil 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------