diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a41a31f3..f0161d40 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -14,6 +14,11 @@ jobs:
continue-on-error: false
steps:
- uses: actions/checkout@v4
+ - name: Crystal Ameba Linter
+ id: crystal-ameba
+ uses: crystal-ameba/github-action@v0.7.1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install Crystal
uses: crystal-lang/install-crystal@v1
with:
@@ -22,8 +27,8 @@ jobs:
run: shards install
- name: Format
run: crystal tool format --check
- - name: Lint
- run: ./bin/ameba
+ #- name: Lint
+ #run: ./bin/ameba
specs:
strategy:
diff --git a/.gitignore b/.gitignore
index 5c7203d9..2683bbbb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,7 @@ lucky
/test-project/
node_modules
/tmp/
+!/fixtures/*/expected/**/*
# Libraries don't need dependency lock
# Dependencies will be locked in application that uses them
diff --git a/fixtures/api_authentication_template/expected/spec/requests/api/me/show_spec.cr b/fixtures/api_authentication_template/expected/spec/requests/api/me/show_spec.cr
new file mode 100644
index 00000000..0e1f91fe
--- /dev/null
+++ b/fixtures/api_authentication_template/expected/spec/requests/api/me/show_spec.cr
@@ -0,0 +1,17 @@
+require "../../../spec_helper"
+
+describe Api::Me::Show do
+ it "returns the signed in user" do
+ user = UserFactory.create
+
+ response = ApiClient.auth(user).exec(Api::Me::Show)
+
+ response.should send_json(200, email: user.email)
+ end
+
+ it "fails if not authenticated" do
+ response = ApiClient.exec(Api::Me::Show)
+
+ response.status_code.should eq(401)
+ end
+end
diff --git a/fixtures/api_authentication_template/expected/spec/requests/api/sign_ins/create_spec.cr b/fixtures/api_authentication_template/expected/spec/requests/api/sign_ins/create_spec.cr
new file mode 100644
index 00000000..520c2dff
--- /dev/null
+++ b/fixtures/api_authentication_template/expected/spec/requests/api/sign_ins/create_spec.cr
@@ -0,0 +1,33 @@
+require "../../../spec_helper"
+
+describe Api::SignIns::Create do
+ it "returns a token" do
+ UserToken.stub_token("fake-token") do
+ user = UserFactory.create
+
+ response = ApiClient.exec(Api::SignIns::Create, user: valid_params(user))
+
+ response.should send_json(200, token: "fake-token")
+ end
+ end
+
+ it "returns an error if credentials are invalid" do
+ user = UserFactory.create
+ invalid_params = valid_params(user).merge(password: "incorrect")
+
+ response = ApiClient.exec(Api::SignIns::Create, user: invalid_params)
+
+ response.should send_json(
+ 400,
+ param: "password",
+ details: "password is wrong"
+ )
+ end
+end
+
+private def valid_params(user : User)
+ {
+ email: user.email,
+ password: "password",
+ }
+end
diff --git a/fixtures/api_authentication_template/expected/spec/requests/api/sign_ups/create_spec.cr b/fixtures/api_authentication_template/expected/spec/requests/api/sign_ups/create_spec.cr
new file mode 100644
index 00000000..2a23542e
--- /dev/null
+++ b/fixtures/api_authentication_template/expected/spec/requests/api/sign_ups/create_spec.cr
@@ -0,0 +1,34 @@
+require "../../../spec_helper"
+
+describe Api::SignUps::Create do
+ it "creates user on sign up" do
+ UserToken.stub_token("fake-token") do
+ response = ApiClient.exec(Api::SignUps::Create, user: valid_params)
+
+ response.should send_json(200, token: "fake-token")
+ new_user = UserQuery.first
+ new_user.email.should eq(valid_params[:email])
+ end
+ end
+
+ it "returns error for invalid params" do
+ invalid_params = valid_params.merge(password_confirmation: "wrong")
+
+ response = ApiClient.exec(Api::SignUps::Create, user: invalid_params)
+
+ UserQuery.new.select_count.should eq(0)
+ response.should send_json(
+ 400,
+ param: "password_confirmation",
+ details: "password_confirmation must match"
+ )
+ end
+end
+
+private def valid_params
+ {
+ email: "test@email.com",
+ password: "password",
+ password_confirmation: "password",
+ }
+end
diff --git a/fixtures/api_authentication_template/expected/src/actions/api/me/show.cr b/fixtures/api_authentication_template/expected/src/actions/api/me/show.cr
new file mode 100644
index 00000000..00602713
--- /dev/null
+++ b/fixtures/api_authentication_template/expected/src/actions/api/me/show.cr
@@ -0,0 +1,5 @@
+class Api::Me::Show < ApiAction
+ get "/api/me" do
+ json UserSerializer.new(current_user)
+ end
+end
diff --git a/fixtures/api_authentication_template/expected/src/actions/api/sign_ins/create.cr b/fixtures/api_authentication_template/expected/src/actions/api/sign_ins/create.cr
new file mode 100644
index 00000000..3670356c
--- /dev/null
+++ b/fixtures/api_authentication_template/expected/src/actions/api/sign_ins/create.cr
@@ -0,0 +1,13 @@
+class Api::SignIns::Create < ApiAction
+ include Api::Auth::SkipRequireAuthToken
+
+ post "/api/sign_ins" do
+ SignInUser.run(params) do |operation, user|
+ if user
+ json({token: UserToken.generate(user)})
+ else
+ raise Avram::InvalidOperationError.new(operation)
+ end
+ end
+ end
+end
diff --git a/fixtures/api_authentication_template/expected/src/actions/api/sign_ups/create.cr b/fixtures/api_authentication_template/expected/src/actions/api/sign_ups/create.cr
new file mode 100644
index 00000000..15bbd04b
--- /dev/null
+++ b/fixtures/api_authentication_template/expected/src/actions/api/sign_ups/create.cr
@@ -0,0 +1,8 @@
+class Api::SignUps::Create < ApiAction
+ include Api::Auth::SkipRequireAuthToken
+
+ post "/api/sign_ups" do
+ user = SignUpUser.create!(params)
+ json({token: UserToken.generate(user)})
+ end
+end
diff --git a/fixtures/api_authentication_template/expected/src/actions/mixins/api/auth/helpers.cr b/fixtures/api_authentication_template/expected/src/actions/mixins/api/auth/helpers.cr
new file mode 100644
index 00000000..6b51cb5a
--- /dev/null
+++ b/fixtures/api_authentication_template/expected/src/actions/mixins/api/auth/helpers.cr
@@ -0,0 +1,28 @@
+module Api::Auth::Helpers
+ # The 'memoize' macro makes sure only one query is issued to find the user
+ memoize def current_user? : User?
+ auth_token.try do |value|
+ user_from_auth_token(value)
+ end
+ end
+
+ private def auth_token : String?
+ bearer_token || token_param
+ end
+
+ private def bearer_token : String?
+ context.request.headers["Authorization"]?
+ .try(&.gsub("Bearer", ""))
+ .try(&.strip)
+ end
+
+ private def token_param : String?
+ params.get?(:auth_token)
+ end
+
+ private def user_from_auth_token(token : String) : User?
+ UserToken.decode_user_id(token).try do |user_id|
+ UserQuery.new.id(user_id).first?
+ end
+ end
+end
diff --git a/fixtures/api_authentication_template/expected/src/actions/mixins/api/auth/require_auth_token.cr b/fixtures/api_authentication_template/expected/src/actions/mixins/api/auth/require_auth_token.cr
new file mode 100644
index 00000000..e018638f
--- /dev/null
+++ b/fixtures/api_authentication_template/expected/src/actions/mixins/api/auth/require_auth_token.cr
@@ -0,0 +1,34 @@
+module Api::Auth::RequireAuthToken
+ macro included
+ before require_auth_token
+ end
+
+ private def require_auth_token
+ if current_user?
+ continue
+ else
+ json auth_error_json, 401
+ end
+ end
+
+ private def auth_error_json
+ ErrorSerializer.new(
+ message: "Not authenticated.",
+ details: auth_error_details
+ )
+ end
+
+ private def auth_error_details : String
+ if auth_token
+ "The provided authentication token was incorrect."
+ else
+ "An authentication token is required. Please include a token in an 'auth_token' param or 'Authorization' header."
+ end
+ end
+
+ # Tells the compiler that the current_user is not nil since we have checked
+ # that the user is signed in
+ private def current_user : User
+ current_user?.as(User)
+ end
+end
diff --git a/fixtures/api_authentication_template/expected/src/actions/mixins/api/auth/skip_require_auth_token.cr b/fixtures/api_authentication_template/expected/src/actions/mixins/api/auth/skip_require_auth_token.cr
new file mode 100644
index 00000000..68098cf5
--- /dev/null
+++ b/fixtures/api_authentication_template/expected/src/actions/mixins/api/auth/skip_require_auth_token.cr
@@ -0,0 +1,10 @@
+module Api::Auth::SkipRequireAuthToken
+ macro included
+ skip require_auth_token
+ end
+
+ # Since sign in is not required, current_user might be nil
+ def current_user : User?
+ current_user?
+ end
+end
diff --git a/fixtures/api_authentication_template/expected/src/models/user_token.cr b/fixtures/api_authentication_template/expected/src/models/user_token.cr
new file mode 100644
index 00000000..65863032
--- /dev/null
+++ b/fixtures/api_authentication_template/expected/src/models/user_token.cr
@@ -0,0 +1,30 @@
+# Generates and decodes JSON Web Tokens for Authenticating users.
+class UserToken
+ Habitat.create { setting stubbed_token : String? }
+ ALGORITHM = JWT::Algorithm::HS256
+
+ def self.generate(user : User) : String
+ payload = {"user_id" => user.id}
+
+ settings.stubbed_token || create_token(payload)
+ end
+
+ def self.create_token(payload)
+ JWT.encode(payload, Lucky::Server.settings.secret_key_base, ALGORITHM)
+ end
+
+ def self.decode_user_id(token : String) : Int64?
+ payload, _header = JWT.decode(token, Lucky::Server.settings.secret_key_base, ALGORITHM)
+ payload["user_id"].to_s.to_i64
+ rescue e : JWT::Error
+ Lucky::Log.dexter.error { {jwt_decode_error: e.message} }
+ nil
+ end
+
+ # Used in tests to return a fake token to test against.
+ def self.stub_token(token : String, &)
+ temp_config(stubbed_token: token) do
+ yield
+ end
+ end
+end
diff --git a/fixtures/api_authentication_template/expected/src/serializers/user_serializer.cr b/fixtures/api_authentication_template/expected/src/serializers/user_serializer.cr
new file mode 100644
index 00000000..1a86f14a
--- /dev/null
+++ b/fixtures/api_authentication_template/expected/src/serializers/user_serializer.cr
@@ -0,0 +1,8 @@
+class UserSerializer < BaseSerializer
+ def initialize(@user : User)
+ end
+
+ def render
+ {email: @user.email}
+ end
+end
diff --git a/fixtures/app_sec_tester_template/expected/spec/flows/security_spec.cr b/fixtures/app_sec_tester_template/expected/spec/flows/security_spec.cr
new file mode 100644
index 00000000..d0432007
--- /dev/null
+++ b/fixtures/app_sec_tester_template/expected/spec/flows/security_spec.cr
@@ -0,0 +1,14 @@
+{% skip_file unless flag?(:with_sec_tests) %}
+# Run these specs with `crystal spec -Dwith_sec_tests`
+
+require "../spec_helper"
+
+describe "SecTester" do
+end
+
+private def scan_with_cleanup(&) : Nil
+ scanner = LuckySecTester.new
+ yield scanner
+ensure
+ scanner.try &.cleanup
+end
diff --git a/fixtures/app_sec_tester_template/expected/spec/setup/sec_tester.cr b/fixtures/app_sec_tester_template/expected/spec/setup/sec_tester.cr
new file mode 100644
index 00000000..bbf9207a
--- /dev/null
+++ b/fixtures/app_sec_tester_template/expected/spec/setup/sec_tester.cr
@@ -0,0 +1,9 @@
+require "lucky_sec_tester"
+
+# Signup for a `BRIGHT_TOKEN` at
+# [NeuraLegion](https://app.neuralegion.com/signup)
+# Read more about the SecTester on https://github.com/luckyframework/lucky_sec_tester
+LuckySecTester.configure do |setting|
+ setting.bright_token = ENV["BRIGHT_TOKEN"]
+ setting.project_id = ENV["BRIGHT_PROJECT_ID"]
+end
diff --git a/fixtures/app_sec_tester_template__browser/expected/spec/flows/security_spec.cr b/fixtures/app_sec_tester_template__browser/expected/spec/flows/security_spec.cr
new file mode 100644
index 00000000..623a9683
--- /dev/null
+++ b/fixtures/app_sec_tester_template__browser/expected/spec/flows/security_spec.cr
@@ -0,0 +1,44 @@
+{% skip_file unless flag?(:with_sec_tests) %}
+# Run these specs with `crystal spec -Dwith_sec_tests`
+
+require "../spec_helper"
+
+describe "SecTester" do
+ it "tests the home page general infra issues" do
+ scan_with_cleanup do |scanner|
+ target = scanner.build_target(Home::Index)
+ scanner.run_check(
+ scan_name: "ref: #{ENV["GITHUB_REF"]?} commit: #{ENV["GITHUB_SHA"]?} run id: #{ENV["GITHUB_RUN_ID"]?}",
+ severity_threshold: SecTester::Severity::Medium,
+ tests: [
+ "header_security", # Testing for header security issues (https://docs.brightsec.com/docs/misconfigured-security-headers)
+ "cookie_security", # Testing for Cookie Security issues (https://docs.brightsec.com/docs/sensitive-cookie-in-https-session-without-secure-attribute)
+ "proto_pollution", # Testing for proto pollution based vulnerabilities (https://docs.brightsec.com/docs/prototype-pollution)
+ "open_buckets", # Testing for open buckets (https://docs.brightsec.com/docs/open-bucket)
+ ],
+ target: target
+ )
+ end
+ end
+
+ it "tests app.js for 3rd party issues" do
+ scan_with_cleanup do |scanner|
+ target = SecTester::Target.new(Lucky::RouteHelper.settings.base_uri + Lucky::AssetHelpers.asset("js/app.js"))
+ scanner.run_check(
+ scan_name: "ref: #{ENV["GITHUB_REF"]?} commit: #{ENV["GITHUB_SHA"]?} run id: #{ENV["GITHUB_RUN_ID"]?}",
+ tests: [
+ "retire_js", # Testing for 3rd party issues (https://docs.brightsec.com/docs/javascript-component-with-known-vulnerabilities)
+ "cve_test", # Testing for known CVEs (https://docs.brightsec.com/docs/cves)
+ ],
+ target: target
+ )
+ end
+ end
+end
+
+private def scan_with_cleanup(&) : Nil
+ scanner = LuckySecTester.new
+ yield scanner
+ensure
+ scanner.try &.cleanup
+end
diff --git a/fixtures/app_sec_tester_template__browser/expected/spec/setup/sec_tester.cr b/fixtures/app_sec_tester_template__browser/expected/spec/setup/sec_tester.cr
new file mode 100644
index 00000000..bbf9207a
--- /dev/null
+++ b/fixtures/app_sec_tester_template__browser/expected/spec/setup/sec_tester.cr
@@ -0,0 +1,9 @@
+require "lucky_sec_tester"
+
+# Signup for a `BRIGHT_TOKEN` at
+# [NeuraLegion](https://app.neuralegion.com/signup)
+# Read more about the SecTester on https://github.com/luckyframework/lucky_sec_tester
+LuckySecTester.configure do |setting|
+ setting.bright_token = ENV["BRIGHT_TOKEN"]
+ setting.project_id = ENV["BRIGHT_PROJECT_ID"]
+end
diff --git a/fixtures/app_sec_tester_template__generate_auth/expected/spec/flows/security_spec.cr b/fixtures/app_sec_tester_template__generate_auth/expected/spec/flows/security_spec.cr
new file mode 100644
index 00000000..8db5ab7f
--- /dev/null
+++ b/fixtures/app_sec_tester_template__generate_auth/expected/spec/flows/security_spec.cr
@@ -0,0 +1,34 @@
+{% skip_file unless flag?(:with_sec_tests) %}
+# Run these specs with `crystal spec -Dwith_sec_tests`
+
+require "../spec_helper"
+
+describe "SecTester" do
+ it "tests the sign_in API for SQLi, and JWT attacks" do
+ scan_with_cleanup do |scanner|
+ api_headers = HTTP::Headers{"Content-Type" => "application/json", "Accept" => "application/json"}
+ target = scanner.build_target(Api::SignIns::Create, headers: api_headers) do |t|
+ t.body = {"user" => {"email" => "aa@aa.com", "password" => "123456789"}}.to_json
+ end
+ scanner.run_check(
+ scan_name: "ref: #{ENV["GITHUB_REF"]?} commit: #{ENV["GITHUB_SHA"]?} run id: #{ENV["GITHUB_RUN_ID"]?}",
+ tests: [
+ "sqli", # Testing for SQL Injection issues (https://docs.brightsec.com/docs/sql-injection)
+ "jwt", # Testing JWT usage (https://docs.brightsec.com/docs/broken-jwt-authentication)
+ "xss", # Testing for Cross Site Scripting attacks (https://docs.brightsec.com/docs/reflective-cross-site-scripting-rxss)
+ "ssrf", # Testing for SSRF (https://docs.brightsec.com/docs/server-side-request-forgery-ssrf)
+ "mass_assignment", # Testing for Mass Assignment issues (https://docs.brightsec.com/docs/mass-assignment)
+ "full_path_disclosure", # Testing for full path disclourse on api error (https://docs.brightsec.com/docs/full-path-disclosure)
+ ],
+ target: target
+ )
+ end
+ end
+end
+
+private def scan_with_cleanup(&) : Nil
+ scanner = LuckySecTester.new
+ yield scanner
+ensure
+ scanner.try &.cleanup
+end
diff --git a/fixtures/app_sec_tester_template__generate_auth/expected/spec/setup/sec_tester.cr b/fixtures/app_sec_tester_template__generate_auth/expected/spec/setup/sec_tester.cr
new file mode 100644
index 00000000..bbf9207a
--- /dev/null
+++ b/fixtures/app_sec_tester_template__generate_auth/expected/spec/setup/sec_tester.cr
@@ -0,0 +1,9 @@
+require "lucky_sec_tester"
+
+# Signup for a `BRIGHT_TOKEN` at
+# [NeuraLegion](https://app.neuralegion.com/signup)
+# Read more about the SecTester on https://github.com/luckyframework/lucky_sec_tester
+LuckySecTester.configure do |setting|
+ setting.bright_token = ENV["BRIGHT_TOKEN"]
+ setting.project_id = ENV["BRIGHT_PROJECT_ID"]
+end
diff --git a/fixtures/base_authentication_src_template/expected/config/authentic.cr b/fixtures/base_authentication_src_template/expected/config/authentic.cr
new file mode 100644
index 00000000..b9efc318
--- /dev/null
+++ b/fixtures/base_authentication_src_template/expected/config/authentic.cr
@@ -0,0 +1,11 @@
+require "./server"
+
+Authentic.configure do |settings|
+ settings.secret_key = Lucky::Server.settings.secret_key_base
+
+ unless LuckyEnv.production?
+ # This value can be between 4 and 31
+ fastest_encryption_possible = 4
+ settings.encryption_cost = fastest_encryption_possible
+ end
+end
diff --git a/fixtures/base_authentication_src_template/expected/db/migrations/.keep b/fixtures/base_authentication_src_template/expected/db/migrations/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/base_authentication_src_template/expected/db/migrations/00000000000001_create_users.cr b/fixtures/base_authentication_src_template/expected/db/migrations/00000000000001_create_users.cr
new file mode 100644
index 00000000..96283bfa
--- /dev/null
+++ b/fixtures/base_authentication_src_template/expected/db/migrations/00000000000001_create_users.cr
@@ -0,0 +1,17 @@
+class CreateUsers::V00000000000001 < Avram::Migrator::Migration::V1
+ def migrate
+ enable_extension "citext"
+
+ create table_for(User) do
+ primary_key id : Int64
+ add_timestamps
+ add email : String, unique: true, case_sensitive: false
+ add encrypted_password : String
+ end
+ end
+
+ def rollback
+ drop table_for(User)
+ disable_extension "citext"
+ end
+end
diff --git a/fixtures/base_authentication_src_template/expected/spec/support/.keep b/fixtures/base_authentication_src_template/expected/spec/support/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/base_authentication_src_template/expected/spec/support/factories/user_factory.cr b/fixtures/base_authentication_src_template/expected/spec/support/factories/user_factory.cr
new file mode 100644
index 00000000..bb837ee1
--- /dev/null
+++ b/fixtures/base_authentication_src_template/expected/spec/support/factories/user_factory.cr
@@ -0,0 +1,6 @@
+class UserFactory < Avram::Factory
+ def initialize
+ email "#{sequence("test-email")}@example.com"
+ encrypted_password Authentic.generate_encrypted_password("password")
+ end
+end
diff --git a/fixtures/base_authentication_src_template/expected/src/models/user.cr b/fixtures/base_authentication_src_template/expected/src/models/user.cr
new file mode 100644
index 00000000..39729bb2
--- /dev/null
+++ b/fixtures/base_authentication_src_template/expected/src/models/user.cr
@@ -0,0 +1,13 @@
+class User < BaseModel
+ include Carbon::Emailable
+ include Authentic::PasswordAuthenticatable
+
+ table do
+ column email : String
+ column encrypted_password : String
+ end
+
+ def emailable : Carbon::Address
+ Carbon::Address.new(email)
+ end
+end
diff --git a/fixtures/base_authentication_src_template/expected/src/operations/.keep b/fixtures/base_authentication_src_template/expected/src/operations/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/base_authentication_src_template/expected/src/operations/mixins/.keep b/fixtures/base_authentication_src_template/expected/src/operations/mixins/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/base_authentication_src_template/expected/src/operations/mixins/password_validations.cr b/fixtures/base_authentication_src_template/expected/src/operations/mixins/password_validations.cr
new file mode 100644
index 00000000..c56b9750
--- /dev/null
+++ b/fixtures/base_authentication_src_template/expected/src/operations/mixins/password_validations.cr
@@ -0,0 +1,12 @@
+module PasswordValidations
+ macro included
+ before_save run_password_validations
+ end
+
+ private def run_password_validations
+ validate_required password, password_confirmation
+ validate_confirmation_of password, with: password_confirmation
+ # 72 is a limitation of BCrypt
+ validate_size_of password, min: 6, max: 72
+ end
+end
diff --git a/fixtures/base_authentication_src_template/expected/src/operations/mixins/user_from_email.cr b/fixtures/base_authentication_src_template/expected/src/operations/mixins/user_from_email.cr
new file mode 100644
index 00000000..862fa9ac
--- /dev/null
+++ b/fixtures/base_authentication_src_template/expected/src/operations/mixins/user_from_email.cr
@@ -0,0 +1,7 @@
+module UserFromEmail
+ private def user_from_email : User?
+ email.value.try do |value|
+ UserQuery.new.email(value).first?
+ end
+ end
+end
diff --git a/fixtures/base_authentication_src_template/expected/src/operations/request_password_reset.cr b/fixtures/base_authentication_src_template/expected/src/operations/request_password_reset.cr
new file mode 100644
index 00000000..4941aa7f
--- /dev/null
+++ b/fixtures/base_authentication_src_template/expected/src/operations/request_password_reset.cr
@@ -0,0 +1,25 @@
+class RequestPasswordReset < Avram::Operation
+ # You can modify this in src/operations/mixins/user_from_email.cr
+ include UserFromEmail
+
+ attribute email : String
+
+ # Run validations and yield the operation and the user if valid
+ def run
+ user = user_from_email
+ validate(user)
+
+ if valid?
+ user
+ else
+ nil
+ end
+ end
+
+ def validate(user : User?)
+ validate_required email
+ if user.nil?
+ email.add_error "is not in our system"
+ end
+ end
+end
diff --git a/fixtures/base_authentication_src_template/expected/src/operations/reset_password.cr b/fixtures/base_authentication_src_template/expected/src/operations/reset_password.cr
new file mode 100644
index 00000000..3bdd3c89
--- /dev/null
+++ b/fixtures/base_authentication_src_template/expected/src/operations/reset_password.cr
@@ -0,0 +1,11 @@
+class ResetPassword < User::SaveOperation
+ # Change password validations in src/operations/mixins/password_validations.cr
+ include PasswordValidations
+
+ attribute password : String
+ attribute password_confirmation : String
+
+ before_save do
+ Authentic.copy_and_encrypt password, to: encrypted_password
+ end
+end
diff --git a/fixtures/base_authentication_src_template/expected/src/operations/sign_in_user.cr b/fixtures/base_authentication_src_template/expected/src/operations/sign_in_user.cr
new file mode 100644
index 00000000..de80342e
--- /dev/null
+++ b/fixtures/base_authentication_src_template/expected/src/operations/sign_in_user.cr
@@ -0,0 +1,40 @@
+class SignInUser < Avram::Operation
+ param_key :user
+ # You can modify this in src/operations/mixins/user_from_email.cr
+ include UserFromEmail
+
+ attribute email : String
+ attribute password : String
+
+ # Run validations and yields the operation and the user if valid
+ def run
+ user = user_from_email
+ validate_credentials(user)
+
+ if valid?
+ user
+ else
+ nil
+ end
+ end
+
+ # `validate_credentials` determines if a user can sign in.
+ #
+ # If desired, you can add additional checks in this method, e.g.
+ #
+ # if user.locked?
+ # email.add_error "is locked out"
+ # end
+ private def validate_credentials(user)
+ if user
+ unless Authentic.correct_password?(user, password.value.to_s)
+ password.add_error "is wrong"
+ end
+ else
+ # Usually ok to say that an email is not in the system:
+ # https://kev.inburke.com/kevin/invalid-username-or-password-useless/
+ # https://github.com/luckyframework/lucky_cli/issues/192
+ email.add_error "is not in our system"
+ end
+ end
+end
diff --git a/fixtures/base_authentication_src_template/expected/src/operations/sign_up_user.cr b/fixtures/base_authentication_src_template/expected/src/operations/sign_up_user.cr
new file mode 100644
index 00000000..8c46fadc
--- /dev/null
+++ b/fixtures/base_authentication_src_template/expected/src/operations/sign_up_user.cr
@@ -0,0 +1,14 @@
+class SignUpUser < User::SaveOperation
+ param_key :user
+ # Change password validations in src/operations/mixins/password_validations.cr
+ include PasswordValidations
+
+ permit_columns email
+ attribute password : String
+ attribute password_confirmation : String
+
+ before_save do
+ validate_uniqueness_of email
+ Authentic.copy_and_encrypt(password, to: encrypted_password) if password.valid?
+ end
+end
diff --git a/fixtures/base_authentication_src_template/expected/src/queries/user_query.cr b/fixtures/base_authentication_src_template/expected/src/queries/user_query.cr
new file mode 100644
index 00000000..8a7e9a7f
--- /dev/null
+++ b/fixtures/base_authentication_src_template/expected/src/queries/user_query.cr
@@ -0,0 +1,2 @@
+class UserQuery < User::BaseQuery
+end
diff --git a/fixtures/browser_authentication_src_template/expected/spec/flows/authentication_spec.cr b/fixtures/browser_authentication_src_template/expected/spec/flows/authentication_spec.cr
new file mode 100644
index 00000000..b22f93cd
--- /dev/null
+++ b/fixtures/browser_authentication_src_template/expected/spec/flows/authentication_spec.cr
@@ -0,0 +1,31 @@
+require "../spec_helper"
+
+describe "Authentication flow", tags: "flow" do
+ it "works" do
+ flow = AuthenticationFlow.new("test@example.com")
+
+ flow.sign_up "password"
+ flow.should_be_signed_in
+ flow.sign_out
+ flow.sign_in "wrong-password"
+ flow.should_have_password_error
+ flow.sign_in "password"
+ flow.should_be_signed_in
+ end
+
+ # This is to show you how to sign in as a user during tests.
+ # Use the `visit` method's `as` option in your tests to sign in as that user.
+ #
+ # Feel free to delete this once you have other tests using the 'as' option.
+ it "allows sign in through backdoor when testing" do
+ user = UserFactory.create
+ flow = BaseFlow.new
+
+ flow.visit Me::Show, as: user
+ should_be_signed_in(flow)
+ end
+end
+
+private def should_be_signed_in(flow)
+ flow.should have_element("@sign-out-button")
+end
diff --git a/fixtures/browser_authentication_src_template/expected/spec/flows/reset_password_spec.cr b/fixtures/browser_authentication_src_template/expected/spec/flows/reset_password_spec.cr
new file mode 100644
index 00000000..b28af34a
--- /dev/null
+++ b/fixtures/browser_authentication_src_template/expected/spec/flows/reset_password_spec.cr
@@ -0,0 +1,18 @@
+require "../spec_helper"
+
+describe "Reset password flow", tags: "flow" do
+ it "works" do
+ user = UserFactory.create
+ flow = ResetPasswordFlow.new(user)
+
+ flow.request_password_reset
+ flow.should_have_sent_reset_email
+ flow.reset_password "new-password"
+ flow.should_be_signed_in
+ flow.sign_out
+ flow.sign_in "wrong-password"
+ flow.should_have_password_error
+ flow.sign_in "new-password"
+ flow.should_be_signed_in
+ end
+end
diff --git a/fixtures/browser_authentication_src_template/expected/spec/support/.keep b/fixtures/browser_authentication_src_template/expected/spec/support/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/browser_authentication_src_template/expected/spec/support/flows/authentication_flow.cr b/fixtures/browser_authentication_src_template/expected/spec/support/flows/authentication_flow.cr
new file mode 100644
index 00000000..183697f9
--- /dev/null
+++ b/fixtures/browser_authentication_src_template/expected/spec/support/flows/authentication_flow.cr
@@ -0,0 +1,45 @@
+class AuthenticationFlow < BaseFlow
+ private getter email
+
+ def initialize(@email : String)
+ end
+
+ def sign_up(password)
+ visit SignUps::New
+ fill_form SignUpUser,
+ email: email,
+ password: password,
+ password_confirmation: password
+ click "@sign-up-button"
+ end
+
+ def sign_out
+ visit Me::Show
+ sign_out_button.click
+ end
+
+ def sign_in(password)
+ visit SignIns::New
+ fill_form SignInUser,
+ email: email,
+ password: password
+ click "@sign-in-button"
+ end
+
+ def should_be_signed_in
+ current_page.should have_element("@sign-out-button")
+ end
+
+ def should_have_password_error
+ current_page.should have_element("body", text: "Password is wrong")
+ end
+
+ private def sign_out_button
+ el("@sign-out-button")
+ end
+
+ # NOTE: this is a shim for readability
+ private def current_page
+ self
+ end
+end
diff --git a/fixtures/browser_authentication_src_template/expected/spec/support/flows/reset_password_flow.cr b/fixtures/browser_authentication_src_template/expected/spec/support/flows/reset_password_flow.cr
new file mode 100644
index 00000000..b1df7104
--- /dev/null
+++ b/fixtures/browser_authentication_src_template/expected/spec/support/flows/reset_password_flow.cr
@@ -0,0 +1,42 @@
+class ResetPasswordFlow < BaseFlow
+ private getter user, authentication_flow
+ delegate sign_in, sign_out, should_have_password_error, should_be_signed_in,
+ to: authentication_flow
+ delegate email, to: user
+
+ def initialize(@user : User)
+ @authentication_flow = AuthenticationFlow.new(user.email)
+ end
+
+ def request_password_reset
+ with_fake_token do
+ visit PasswordResetRequests::New
+ fill_form RequestPasswordReset,
+ email: email
+ click "@request-password-reset-button"
+ end
+ end
+
+ def should_have_sent_reset_email
+ with_fake_token do
+ user = UserQuery.new.email(email).first
+ PasswordResetRequestEmail.new(user).should be_delivered
+ end
+ end
+
+ def reset_password(password)
+ user = UserQuery.new.email(email).first
+ token = Authentic.generate_password_reset_token(user)
+ visit PasswordResets::New.with(user.id, token)
+ fill_form ResetPassword,
+ password: password,
+ password_confirmation: password
+ click "@update-password-button"
+ end
+
+ private def with_fake_token(&)
+ PasswordResetRequestEmail.temp_config(stubbed_token: "fake") do
+ yield
+ end
+ end
+end
diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/me/show.cr b/fixtures/browser_authentication_src_template/expected/src/actions/me/show.cr
new file mode 100644
index 00000000..5e35848e
--- /dev/null
+++ b/fixtures/browser_authentication_src_template/expected/src/actions/me/show.cr
@@ -0,0 +1,5 @@
+class Me::Show < BrowserAction
+ get "/me" do
+ html ShowPage
+ end
+end
diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/mixins/.keep b/fixtures/browser_authentication_src_template/expected/src/actions/mixins/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/allow_guests.cr b/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/allow_guests.cr
new file mode 100644
index 00000000..3961399b
--- /dev/null
+++ b/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/allow_guests.cr
@@ -0,0 +1,10 @@
+module Auth::AllowGuests
+ macro included
+ skip require_sign_in
+ end
+
+ # Since sign in is not required, current_user might be nil
+ def current_user : User?
+ current_user?
+ end
+end
diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/password_resets/base.cr b/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/password_resets/base.cr
new file mode 100644
index 00000000..77166a97
--- /dev/null
+++ b/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/password_resets/base.cr
@@ -0,0 +1,7 @@
+module Auth::PasswordResets::Base
+ macro included
+ include Auth::RedirectSignedInUsers
+ include Auth::PasswordResets::FindUser
+ include Auth::PasswordResets::RequireToken
+ end
+end
diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/password_resets/find_user.cr b/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/password_resets/find_user.cr
new file mode 100644
index 00000000..cab02d5c
--- /dev/null
+++ b/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/password_resets/find_user.cr
@@ -0,0 +1,5 @@
+module Auth::PasswordResets::FindUser
+ private def user : User
+ UserQuery.find(user_id)
+ end
+end
diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/password_resets/require_token.cr b/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/password_resets/require_token.cr
new file mode 100644
index 00000000..15da4238
--- /dev/null
+++ b/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/password_resets/require_token.cr
@@ -0,0 +1,17 @@
+module Auth::PasswordResets::RequireToken
+ macro included
+ before require_valid_password_reset_token
+ end
+
+ abstract def token : String
+ abstract def user : User
+
+ private def require_valid_password_reset_token
+ if Authentic.valid_password_reset_token?(user, token)
+ continue
+ else
+ flash.failure = "The password reset link is incorrect or expired. Please try again."
+ redirect to: PasswordResetRequests::New
+ end
+ end
+end
diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/password_resets/token_from_session.cr b/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/password_resets/token_from_session.cr
new file mode 100644
index 00000000..820b91bc
--- /dev/null
+++ b/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/password_resets/token_from_session.cr
@@ -0,0 +1,5 @@
+module Auth::PasswordResets::TokenFromSession
+ private def token : String
+ session.get?(:password_reset_token) || raise "Password reset token not found in session"
+ end
+end
diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/redirect_signed_in_users.cr b/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/redirect_signed_in_users.cr
new file mode 100644
index 00000000..546bf7bb
--- /dev/null
+++ b/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/redirect_signed_in_users.cr
@@ -0,0 +1,19 @@
+module Auth::RedirectSignedInUsers
+ macro included
+ include Auth::AllowGuests
+ before redirect_signed_in_users
+ end
+
+ private def redirect_signed_in_users
+ if current_user?
+ flash.success = "You are already signed in"
+ redirect to: Home::Index
+ else
+ continue
+ end
+ end
+
+ # current_user returns nil because signed in users are redirected.
+ def current_user
+ end
+end
diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/require_sign_in.cr b/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/require_sign_in.cr
new file mode 100644
index 00000000..27a6f5ea
--- /dev/null
+++ b/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/require_sign_in.cr
@@ -0,0 +1,21 @@
+module Auth::RequireSignIn
+ macro included
+ before require_sign_in
+ end
+
+ private def require_sign_in
+ if current_user?
+ continue
+ else
+ Authentic.remember_requested_path(self)
+ flash.info = "Please sign in first"
+ redirect to: SignIns::New
+ end
+ end
+
+ # Tells the compiler that the current_user is not nil since we have checked
+ # that the user is signed in
+ private def current_user : User
+ current_user?.as(User)
+ end
+end
diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/test_backdoor.cr b/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/test_backdoor.cr
new file mode 100644
index 00000000..68c9d91a
--- /dev/null
+++ b/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/test_backdoor.cr
@@ -0,0 +1,13 @@
+module Auth::TestBackdoor
+ macro included
+ before test_backdoor
+ end
+
+ private def test_backdoor
+ if LuckyEnv.test? && (user_id = params.get?(:backdoor_user_id))
+ user = UserQuery.find(user_id)
+ sign_in user
+ end
+ continue
+ end
+end
diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/password_reset_requests/create.cr b/fixtures/browser_authentication_src_template/expected/src/actions/password_reset_requests/create.cr
new file mode 100644
index 00000000..8f3c5130
--- /dev/null
+++ b/fixtures/browser_authentication_src_template/expected/src/actions/password_reset_requests/create.cr
@@ -0,0 +1,15 @@
+class PasswordResetRequests::Create < BrowserAction
+ include Auth::RedirectSignedInUsers
+
+ post "/password_reset_requests" do
+ RequestPasswordReset.run(params) do |operation, user|
+ if user
+ PasswordResetRequestEmail.new(user).deliver
+ flash.success = "You should receive an email on how to reset your password shortly"
+ redirect SignIns::New
+ else
+ html NewPage, operation: operation
+ end
+ end
+ end
+end
diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/password_reset_requests/new.cr b/fixtures/browser_authentication_src_template/expected/src/actions/password_reset_requests/new.cr
new file mode 100644
index 00000000..7d16a7dd
--- /dev/null
+++ b/fixtures/browser_authentication_src_template/expected/src/actions/password_reset_requests/new.cr
@@ -0,0 +1,7 @@
+class PasswordResetRequests::New < BrowserAction
+ include Auth::RedirectSignedInUsers
+
+ get "/password_reset_requests/new" do
+ html NewPage, operation: RequestPasswordReset.new
+ end
+end
diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/password_resets/create.cr b/fixtures/browser_authentication_src_template/expected/src/actions/password_resets/create.cr
new file mode 100644
index 00000000..da1e711b
--- /dev/null
+++ b/fixtures/browser_authentication_src_template/expected/src/actions/password_resets/create.cr
@@ -0,0 +1,17 @@
+class PasswordResets::Create < BrowserAction
+ include Auth::PasswordResets::Base
+ include Auth::PasswordResets::TokenFromSession
+
+ post "/password_resets/:user_id" do
+ ResetPassword.update(user, params) do |operation, user|
+ if operation.saved?
+ session.delete(:password_reset_token)
+ sign_in user
+ flash.success = "Your password has been reset"
+ redirect to: Home::Index
+ else
+ html NewPage, operation: operation, user_id: user_id.to_i64
+ end
+ end
+ end
+end
diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/password_resets/edit.cr b/fixtures/browser_authentication_src_template/expected/src/actions/password_resets/edit.cr
new file mode 100644
index 00000000..9408109c
--- /dev/null
+++ b/fixtures/browser_authentication_src_template/expected/src/actions/password_resets/edit.cr
@@ -0,0 +1,8 @@
+class PasswordResets::Edit < BrowserAction
+ include Auth::PasswordResets::Base
+ include Auth::PasswordResets::TokenFromSession
+
+ get "/password_resets/:user_id/edit" do
+ html NewPage, operation: ResetPassword.new, user_id: user_id.to_i64
+ end
+end
diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/password_resets/new.cr b/fixtures/browser_authentication_src_template/expected/src/actions/password_resets/new.cr
new file mode 100644
index 00000000..55034686
--- /dev/null
+++ b/fixtures/browser_authentication_src_template/expected/src/actions/password_resets/new.cr
@@ -0,0 +1,20 @@
+class PasswordResets::New < BrowserAction
+ include Auth::PasswordResets::Base
+
+ param token : String
+
+ get "/password_resets/:user_id" do
+ redirect_to_edit_form_without_token_param
+ end
+
+ # This is to prevent password reset tokens from being scraped in the HTTP Referer header
+ # See more info here: https://github.com/thoughtbot/clearance/pull/707
+ private def redirect_to_edit_form_without_token_param
+ make_token_available_to_future_actions
+ redirect to: PasswordResets::Edit.with(user_id)
+ end
+
+ private def make_token_available_to_future_actions
+ session.set(:password_reset_token, token)
+ end
+end
diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/sign_ins/create.cr b/fixtures/browser_authentication_src_template/expected/src/actions/sign_ins/create.cr
new file mode 100644
index 00000000..af225888
--- /dev/null
+++ b/fixtures/browser_authentication_src_template/expected/src/actions/sign_ins/create.cr
@@ -0,0 +1,16 @@
+class SignIns::Create < BrowserAction
+ include Auth::RedirectSignedInUsers
+
+ post "/sign_in" do
+ SignInUser.run(params) do |operation, authenticated_user|
+ if authenticated_user
+ sign_in(authenticated_user)
+ flash.success = "You're now signed in"
+ Authentic.redirect_to_originally_requested_path(self, fallback: Home::Index)
+ else
+ flash.failure = "Sign in failed"
+ html NewPage, operation: operation
+ end
+ end
+ end
+end
diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/sign_ins/delete.cr b/fixtures/browser_authentication_src_template/expected/src/actions/sign_ins/delete.cr
new file mode 100644
index 00000000..8d34612f
--- /dev/null
+++ b/fixtures/browser_authentication_src_template/expected/src/actions/sign_ins/delete.cr
@@ -0,0 +1,7 @@
+class SignIns::Delete < BrowserAction
+ delete "/sign_out" do
+ sign_out
+ flash.info = "You have been signed out"
+ redirect to: SignIns::New
+ end
+end
diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/sign_ins/new.cr b/fixtures/browser_authentication_src_template/expected/src/actions/sign_ins/new.cr
new file mode 100644
index 00000000..3275b40f
--- /dev/null
+++ b/fixtures/browser_authentication_src_template/expected/src/actions/sign_ins/new.cr
@@ -0,0 +1,7 @@
+class SignIns::New < BrowserAction
+ include Auth::RedirectSignedInUsers
+
+ get "/sign_in" do
+ html NewPage, operation: SignInUser.new
+ end
+end
diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/sign_ups/create.cr b/fixtures/browser_authentication_src_template/expected/src/actions/sign_ups/create.cr
new file mode 100644
index 00000000..a291ca68
--- /dev/null
+++ b/fixtures/browser_authentication_src_template/expected/src/actions/sign_ups/create.cr
@@ -0,0 +1,16 @@
+class SignUps::Create < BrowserAction
+ include Auth::RedirectSignedInUsers
+
+ post "/sign_up" do
+ SignUpUser.create(params) do |operation, user|
+ if user
+ flash.info = "Thanks for signing up"
+ sign_in(user)
+ redirect to: Home::Index
+ else
+ flash.info = "Couldn't sign you up"
+ html NewPage, operation: operation
+ end
+ end
+ end
+end
diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/sign_ups/new.cr b/fixtures/browser_authentication_src_template/expected/src/actions/sign_ups/new.cr
new file mode 100644
index 00000000..2299df6e
--- /dev/null
+++ b/fixtures/browser_authentication_src_template/expected/src/actions/sign_ups/new.cr
@@ -0,0 +1,7 @@
+class SignUps::New < BrowserAction
+ include Auth::RedirectSignedInUsers
+
+ get "/sign_up" do
+ html NewPage, operation: SignUpUser.new
+ end
+end
diff --git a/fixtures/browser_authentication_src_template/expected/src/emails/password_reset_request_email.cr b/fixtures/browser_authentication_src_template/expected/src/emails/password_reset_request_email.cr
new file mode 100644
index 00000000..a41c8ba1
--- /dev/null
+++ b/fixtures/browser_authentication_src_template/expected/src/emails/password_reset_request_email.cr
@@ -0,0 +1,13 @@
+class PasswordResetRequestEmail < BaseEmail
+ Habitat.create { setting stubbed_token : String? }
+ delegate stubbed_token, to: :settings
+
+ def initialize(@user : User)
+ @token = stubbed_token || Authentic.generate_password_reset_token(@user)
+ end
+
+ to @user
+ from "myapp@support.com" # or set a default in src/emails/base_email.cr
+ subject "Reset your password"
+ templates html, text
+end
diff --git a/fixtures/browser_authentication_src_template/expected/src/emails/templates/password_reset_request_email/html.ecr b/fixtures/browser_authentication_src_template/expected/src/emails/templates/password_reset_request_email/html.ecr
new file mode 100644
index 00000000..00c24fc9
--- /dev/null
+++ b/fixtures/browser_authentication_src_template/expected/src/emails/templates/password_reset_request_email/html.ecr
@@ -0,0 +1,3 @@
+
Please reset your password
+
+Reset password
diff --git a/fixtures/browser_authentication_src_template/expected/src/emails/templates/password_reset_request_email/text.ecr b/fixtures/browser_authentication_src_template/expected/src/emails/templates/password_reset_request_email/text.ecr
new file mode 100644
index 00000000..7a7a0ab7
--- /dev/null
+++ b/fixtures/browser_authentication_src_template/expected/src/emails/templates/password_reset_request_email/text.ecr
@@ -0,0 +1,3 @@
+Please reset your password:
+
+<%= PasswordResets::New.url(@user.id, @token) %>
diff --git a/fixtures/browser_authentication_src_template/expected/src/pages/auth_layout.cr b/fixtures/browser_authentication_src_template/expected/src/pages/auth_layout.cr
new file mode 100644
index 00000000..c2ac1b09
--- /dev/null
+++ b/fixtures/browser_authentication_src_template/expected/src/pages/auth_layout.cr
@@ -0,0 +1,27 @@
+abstract class AuthLayout
+ include Lucky::HTMLPage
+
+ abstract def content
+ abstract def page_title
+
+ # The default page title. It is passed to `Shared::LayoutHead`.
+ #
+ # Add a `page_title` method to pages to override it. You can also remove
+ # This method so every page is required to have its own page title.
+ def page_title
+ "Welcome"
+ end
+
+ def render
+ html_doctype
+
+ html lang: "en" do
+ mount Shared::LayoutHead, page_title: page_title
+
+ body do
+ mount Shared::FlashMessages, context.flash
+ content
+ end
+ end
+ end
+end
diff --git a/fixtures/browser_authentication_src_template/expected/src/pages/main_layout.cr b/fixtures/browser_authentication_src_template/expected/src/pages/main_layout.cr
new file mode 100644
index 00000000..06a1ed7a
--- /dev/null
+++ b/fixtures/browser_authentication_src_template/expected/src/pages/main_layout.cr
@@ -0,0 +1,45 @@
+abstract class MainLayout
+ include Lucky::HTMLPage
+
+ # 'needs current_user : User' makes it so that the current_user
+ # is always required for pages using MainLayout
+ needs current_user : User
+
+ abstract def content
+ abstract def page_title
+
+ # MainLayout defines a default 'page_title'.
+ #
+ # Add a 'page_title' method to your indivual pages to customize each page's
+ # title.
+ #
+ # Or, if you want to require every page to set a title, change the
+ # 'page_title' method in this layout to:
+ #
+ # abstract def page_title : String
+ #
+ # This will force pages to define their own 'page_title' method.
+ def page_title
+ "Welcome"
+ end
+
+ def render
+ html_doctype
+
+ html lang: "en" do
+ mount Shared::LayoutHead, page_title: page_title
+
+ body do
+ mount Shared::FlashMessages, context.flash
+ render_signed_in_user
+ content
+ end
+ end
+ end
+
+ private def render_signed_in_user
+ text current_user.email
+ text " - "
+ link "Sign out", to: SignIns::Delete, flow_id: "sign-out-button"
+ end
+end
diff --git a/fixtures/browser_authentication_src_template/expected/src/pages/me/show_page.cr b/fixtures/browser_authentication_src_template/expected/src/pages/me/show_page.cr
new file mode 100644
index 00000000..6a6bd879
--- /dev/null
+++ b/fixtures/browser_authentication_src_template/expected/src/pages/me/show_page.cr
@@ -0,0 +1,21 @@
+class Me::ShowPage < MainLayout
+ def content
+ h1 "This is your profile"
+ h3 "Email: #{@current_user.email}"
+ helpful_tips
+ end
+
+ private def helpful_tips
+ h3 "Next, you may want to:"
+ ul do
+ li { link_to_authentication_guides }
+ li "Modify this page: src/pages/me/show_page.cr"
+ li "Change where you go after sign in: src/actions/home/index.cr"
+ end
+ end
+
+ private def link_to_authentication_guides
+ a "Check out the authentication guides",
+ href: "https://luckyframework.org/guides/authentication"
+ end
+end
diff --git a/fixtures/browser_authentication_src_template/expected/src/pages/password_reset_requests/new_page.cr b/fixtures/browser_authentication_src_template/expected/src/pages/password_reset_requests/new_page.cr
new file mode 100644
index 00000000..368784c9
--- /dev/null
+++ b/fixtures/browser_authentication_src_template/expected/src/pages/password_reset_requests/new_page.cr
@@ -0,0 +1,15 @@
+class PasswordResetRequests::NewPage < AuthLayout
+ needs operation : RequestPasswordReset
+
+ def content
+ h1 "Reset your password"
+ render_form(@operation)
+ end
+
+ private def render_form(op)
+ form_for PasswordResetRequests::Create do
+ mount Shared::Field, attribute: op.email, label_text: "Email", &.email_input
+ submit "Reset Password", flow_id: "request-password-reset-button"
+ end
+ end
+end
diff --git a/fixtures/browser_authentication_src_template/expected/src/pages/password_resets/new_page.cr b/fixtures/browser_authentication_src_template/expected/src/pages/password_resets/new_page.cr
new file mode 100644
index 00000000..16a6635e
--- /dev/null
+++ b/fixtures/browser_authentication_src_template/expected/src/pages/password_resets/new_page.cr
@@ -0,0 +1,18 @@
+class PasswordResets::NewPage < AuthLayout
+ needs operation : ResetPassword
+ needs user_id : Int64
+
+ def content
+ h1 "Reset your password"
+ render_password_reset_form(@operation)
+ end
+
+ private def render_password_reset_form(op)
+ form_for PasswordResets::Create.with(@user_id) do
+ mount Shared::Field, attribute: op.password, label_text: "Password", &.password_input(autofocus: "true")
+ mount Shared::Field, attribute: op.password_confirmation, label_text: "Confirm Password", &.password_input
+
+ submit "Update Password", flow_id: "update-password-button"
+ end
+ end
+end
diff --git a/fixtures/browser_authentication_src_template/expected/src/pages/sign_ins/new_page.cr b/fixtures/browser_authentication_src_template/expected/src/pages/sign_ins/new_page.cr
new file mode 100644
index 00000000..10188135
--- /dev/null
+++ b/fixtures/browser_authentication_src_template/expected/src/pages/sign_ins/new_page.cr
@@ -0,0 +1,23 @@
+class SignIns::NewPage < AuthLayout
+ needs operation : SignInUser
+
+ def content
+ h1 "Sign In"
+ render_sign_in_form(@operation)
+ end
+
+ private def render_sign_in_form(op)
+ form_for SignIns::Create do
+ sign_in_fields(op)
+ submit "Sign In", flow_id: "sign-in-button"
+ end
+ link "Reset password", to: PasswordResetRequests::New
+ text " | "
+ link "Sign up", to: SignUps::New
+ end
+
+ private def sign_in_fields(op)
+ mount Shared::Field, attribute: op.email, label_text: "Email", &.email_input(autofocus: "true")
+ mount Shared::Field, attribute: op.password, label_text: "Password", &.password_input
+ end
+end
diff --git a/fixtures/browser_authentication_src_template/expected/src/pages/sign_ups/new_page.cr b/fixtures/browser_authentication_src_template/expected/src/pages/sign_ups/new_page.cr
new file mode 100644
index 00000000..24f6cb25
--- /dev/null
+++ b/fixtures/browser_authentication_src_template/expected/src/pages/sign_ups/new_page.cr
@@ -0,0 +1,22 @@
+class SignUps::NewPage < AuthLayout
+ needs operation : SignUpUser
+
+ def content
+ h1 "Sign Up"
+ render_sign_up_form(@operation)
+ end
+
+ private def render_sign_up_form(op)
+ form_for SignUps::Create do
+ sign_up_fields(op)
+ submit "Sign Up", flow_id: "sign-up-button"
+ end
+ link "Sign in instead", to: SignIns::New
+ end
+
+ private def sign_up_fields(op)
+ mount Shared::Field, attribute: op.email, label_text: "Email", &.email_input(autofocus: "true")
+ mount Shared::Field, attribute: op.password, label_text: "Password", &.password_input
+ mount Shared::Field, attribute: op.password_confirmation, label_text: "Confirm Password", &.password_input
+ end
+end
diff --git a/fixtures/browser_src_template/expected/bs-config.js b/fixtures/browser_src_template/expected/bs-config.js
new file mode 100644
index 00000000..dbba89ce
--- /dev/null
+++ b/fixtures/browser_src_template/expected/bs-config.js
@@ -0,0 +1,26 @@
+/*
+ | Browser-sync config file
+ |
+ | For up-to-date information about the options:
+ | http://www.browsersync.io/docs/options/
+ |
+ */
+
+module.exports = {
+ snippetOptions: {
+ rule: {
+ match: /<\/head>/i,
+ fn: function (snippet, match) {
+ return snippet + match;
+ }
+ }
+ },
+ files: ["public/css/**/*.css", "public/js/**/*.js"],
+ watchEvents: ["change"],
+ open: false,
+ browser: "default",
+ ghostMode: false,
+ ui: false,
+ online: false,
+ logConnections: false
+};
diff --git a/fixtures/browser_src_template/expected/config/html_page.cr b/fixtures/browser_src_template/expected/config/html_page.cr
new file mode 100644
index 00000000..dca168e5
--- /dev/null
+++ b/fixtures/browser_src_template/expected/config/html_page.cr
@@ -0,0 +1,3 @@
+Lucky::HTMLPage.configure do |settings|
+ settings.render_component_comments = !LuckyEnv.production?
+end
diff --git a/fixtures/browser_src_template/expected/db/migrations/.keep b/fixtures/browser_src_template/expected/db/migrations/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/browser_src_template/expected/package.json b/fixtures/browser_src_template/expected/package.json
new file mode 100644
index 00000000..fd6b12fd
--- /dev/null
+++ b/fixtures/browser_src_template/expected/package.json
@@ -0,0 +1,24 @@
+{
+ "license": "UNLICENSED",
+ "private": true,
+ "dependencies": {
+ "@rails/ujs": "^6.0.0",
+ "modern-normalize": "^1.1.0"
+ },
+ "scripts": {
+ "heroku-postbuild": "yarn prod",
+ "dev": "yarn run mix",
+ "watch": "yarn run mix watch",
+ "prod": "yarn run mix --production"
+ },
+ "devDependencies": {
+ "@babel/compat-data": "^7.9.0",
+ "browser-sync": "^2.26.7",
+ "compression-webpack-plugin": "^7.0.0",
+ "laravel-mix": "^6.0.0",
+ "postcss": "^8.1.0",
+ "resolve-url-loader": "^3.1.1",
+ "sass": "^1.26.10",
+ "sass-loader": "^10.0.2"
+ }
+}
diff --git a/fixtures/browser_src_template/expected/public/assets/images/.keep b/fixtures/browser_src_template/expected/public/assets/images/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/browser_src_template/expected/public/favicon.ico b/fixtures/browser_src_template/expected/public/favicon.ico
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/browser_src_template/expected/public/mix-manifest.json b/fixtures/browser_src_template/expected/public/mix-manifest.json
new file mode 100644
index 00000000..d70320ad
--- /dev/null
+++ b/fixtures/browser_src_template/expected/public/mix-manifest.json
@@ -0,0 +1,4 @@
+{
+ "/css/app.css": "/css/app.css",
+ "/js/app.js": "/js/app.js"
+}
diff --git a/fixtures/browser_src_template/expected/public/robots.txt b/fixtures/browser_src_template/expected/public/robots.txt
new file mode 100644
index 00000000..12009050
--- /dev/null
+++ b/fixtures/browser_src_template/expected/public/robots.txt
@@ -0,0 +1,4 @@
+# Learn more about robots.txt: https://www.robotstxt.org/robotstxt.html
+User-agent: *
+# 'Disallow' with an empty value allows all paths to be crawled
+Disallow:
diff --git a/fixtures/browser_src_template/expected/spec/flows/.keep b/fixtures/browser_src_template/expected/spec/flows/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/browser_src_template/expected/spec/setup/.keep b/fixtures/browser_src_template/expected/spec/setup/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/browser_src_template/expected/spec/setup/configure_lucky_flow.cr b/fixtures/browser_src_template/expected/spec/setup/configure_lucky_flow.cr
new file mode 100644
index 00000000..504a3d34
--- /dev/null
+++ b/fixtures/browser_src_template/expected/spec/setup/configure_lucky_flow.cr
@@ -0,0 +1,37 @@
+# For more detailed documentation, visit
+# https://luckyframework.org/guides/testing/html-and-interactivity
+
+LuckyFlow.configure do |settings|
+ settings.stop_retrying_after = 200.milliseconds
+ settings.base_uri = Lucky::RouteHelper.settings.base_uri
+
+ # LuckyFlow will install the chromedriver for you located in
+ # ~./webdrivers/. Uncomment this to point to a specific driver
+ # settings.driver_path = "/path/to/specific/chromedriver"
+end
+
+# By default, LuckyFlow is set in "headless" mode (no browser window shown).
+# Uncomment this to enable running `LuckyFlow` in a Google Chrome window instead.
+# Be sure to disable for CI.
+#
+# LuckyFlow.default_driver = "chrome"
+
+# LuckyFlow uses a registry for each driver. By default, chrome, and headless_chrome
+# are available. If you'd like to register your own custom driver, you can register
+# it here.
+#
+# LuckyFlow::Registry.register :firefox do
+# # add your custom driver here
+# end
+
+# Setup specs to allow you to change the driver on the fly
+# per spec by setting a tag on specific specs. Requires the
+# driver to be registered through `LuckyFlow::Registry` first.
+#
+# ```
+# it "uses headless_chrome" do
+# end
+# it "uses webless", tags: "webless" do
+# end
+# ```
+LuckyFlow::Spec.setup
diff --git a/fixtures/browser_src_template/expected/spec/support/.keep b/fixtures/browser_src_template/expected/spec/support/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/browser_src_template/expected/spec/support/factories/.keep b/fixtures/browser_src_template/expected/spec/support/factories/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/browser_src_template/expected/spec/support/flows/base_flow.cr b/fixtures/browser_src_template/expected/spec/support/flows/base_flow.cr
new file mode 100644
index 00000000..93709b25
--- /dev/null
+++ b/fixtures/browser_src_template/expected/spec/support/flows/base_flow.cr
@@ -0,0 +1,3 @@
+# Add methods that all or most Flows need to share
+class BaseFlow < LuckyFlow
+end
diff --git a/fixtures/browser_src_template/expected/src/actions/browser_action.cr b/fixtures/browser_src_template/expected/src/actions/browser_action.cr
new file mode 100644
index 00000000..674f2088
--- /dev/null
+++ b/fixtures/browser_src_template/expected/src/actions/browser_action.cr
@@ -0,0 +1,45 @@
+abstract class BrowserAction < Lucky::Action
+ include Lucky::ProtectFromForgery
+
+ # By default all actions are required to use underscores.
+ # Add `include Lucky::SkipRouteStyleCheck` to your actions if you wish to ignore this check for specific routes.
+ include Lucky::EnforceUnderscoredRoute
+
+ # This module disables Google FLoC by setting the
+ # [Permissions-Policy](https://github.com/WICG/floc) HTTP header to `interest-cohort=()`.
+ #
+ # This header is a part of Google's Federated Learning of Cohorts (FLoC) which is used
+ # to track browsing history instead of using 3rd-party cookies.
+ #
+ # Remove this include if you want to use the FLoC tracking.
+ include Lucky::SecureHeaders::DisableFLoC
+
+ accepted_formats [:html, :json], default: :html
+
+ # This module provides current_user, sign_in, and sign_out methods
+ include Authentic::ActionHelpers(User)
+
+ # When testing you can skip normal sign in by using `visit` with the `as` param
+ #
+ # flow.visit Me::Show, as: UserFactory.create
+ include Auth::TestBackdoor
+
+ # By default all actions that inherit 'BrowserAction' require sign in.
+ #
+ # You can remove the 'include Auth::RequireSignIn' below to allow anyone to
+ # access actions that inherit from 'BrowserAction' or you can
+ # 'include Auth::AllowGuests' in individual actions to skip sign in.
+ include Auth::RequireSignIn
+
+ # `expose` means that `current_user` will be passed to pages automatically.
+ #
+ # In default Lucky apps, the `MainLayout` declares it `needs current_user : User`
+ # so that any page that inherits from MainLayout can use the `current_user`
+ expose current_user
+
+ # This method tells Authentic how to find the current user
+ # The 'memoize' macro makes sure only one query is issued to find the user
+ private memoize def find_current_user(id : String | User::PrimaryKeyType) : User?
+ UserQuery.new.id(id).first?
+ end
+end
diff --git a/fixtures/browser_src_template/expected/src/components/.keep b/fixtures/browser_src_template/expected/src/components/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/browser_src_template/expected/src/components/base_component.cr b/fixtures/browser_src_template/expected/src/components/base_component.cr
new file mode 100644
index 00000000..c9829b48
--- /dev/null
+++ b/fixtures/browser_src_template/expected/src/components/base_component.cr
@@ -0,0 +1,2 @@
+abstract class BaseComponent < Lucky::BaseComponent
+end
diff --git a/fixtures/browser_src_template/expected/src/components/shared/field.cr b/fixtures/browser_src_template/expected/src/components/shared/field.cr
new file mode 100644
index 00000000..5c32e8a8
--- /dev/null
+++ b/fixtures/browser_src_template/expected/src/components/shared/field.cr
@@ -0,0 +1,57 @@
+# This component is used to make it easier to render the same fields styles
+# throughout your app.
+#
+# Extensive documentation at: https://luckyframework.org/guides/frontend/html-forms#shared-components
+#
+# ## Basic usage:
+#
+# # Renders a text input by default and will guess the label name "Name"
+# mount Shared::Field, op.name
+# # Call any of the input methods on the block
+# mount Shared::Field, op.email, &.email_input
+# # Add other HTML attributes
+# mount Shared::Field, op.email, &.email_input(autofocus: "true")
+# # Pass an explicit label name
+# mount Shared::Field, attribute: op.username, label_text: "Your username"
+#
+# ## Customization
+#
+# You can customize this component so that fields render like you expect.
+# For example, you might wrap it in a div with a "field-wrapper" class.
+#
+# div class: "field-wrapper"
+# label_for field
+# yield field
+# mount Shared::FieldErrors, field
+# end
+#
+# You may also want to have more components if your fields look
+# different in different parts of your app, e.g. `CompactField` or
+# `InlineTextField`
+class Shared::Field(T) < BaseComponent
+ # Raises a helpful error if component receives an unpermitted attribute
+ include Lucky::CatchUnpermittedAttribute
+
+ needs attribute : Avram::PermittedAttribute(T)
+ needs label_text : String?
+
+ def render(&)
+ label_for attribute, label_text
+
+ # You can add more default options here. For example:
+ #
+ # tag_defaults field: attribute, class: "input"
+ #
+ # Will add the class "input" to the generated HTML.
+ tag_defaults field: attribute do |tag_builder|
+ yield tag_builder
+ end
+
+ mount Shared::FieldErrors, attribute
+ end
+
+ # Use a text_input by default
+ def render
+ render &.text_input
+ end
+end
diff --git a/fixtures/browser_src_template/expected/src/components/shared/field_errors.cr b/fixtures/browser_src_template/expected/src/components/shared/field_errors.cr
new file mode 100644
index 00000000..3f2937a0
--- /dev/null
+++ b/fixtures/browser_src_template/expected/src/components/shared/field_errors.cr
@@ -0,0 +1,16 @@
+class Shared::FieldErrors(T) < BaseComponent
+ needs attribute : Avram::PermittedAttribute(T)
+
+ # Customize the markup and styles to match your application
+ def render
+ unless attribute.valid?
+ div class: "error" do
+ text "#{label_text} #{attribute.errors.first}"
+ end
+ end
+ end
+
+ def label_text : String
+ Wordsmith::Inflector.humanize(attribute.name.to_s)
+ end
+end
diff --git a/fixtures/browser_src_template/expected/src/components/shared/flash_messages.cr b/fixtures/browser_src_template/expected/src/components/shared/flash_messages.cr
new file mode 100644
index 00000000..bc44440d
--- /dev/null
+++ b/fixtures/browser_src_template/expected/src/components/shared/flash_messages.cr
@@ -0,0 +1,11 @@
+class Shared::FlashMessages < BaseComponent
+ needs flash : Lucky::FlashStore
+
+ def render
+ flash.each do |flash_type, flash_message|
+ div class: "flash-#{flash_type}", flow_id: "flash" do
+ text flash_message
+ end
+ end
+ end
+end
diff --git a/fixtures/browser_src_template/expected/src/components/shared/layout_head.cr b/fixtures/browser_src_template/expected/src/components/shared/layout_head.cr
new file mode 100644
index 00000000..5a053315
--- /dev/null
+++ b/fixtures/browser_src_template/expected/src/components/shared/layout_head.cr
@@ -0,0 +1,18 @@
+class Shared::LayoutHead < BaseComponent
+ needs page_title : String
+
+ def render
+ head do
+ utf8_charset
+ title "My App - #{@page_title}"
+ css_link asset("css/app.css")
+ js_link asset("js/app.js"), defer: "true"
+ csrf_meta_tags
+ responsive_meta_tag
+
+ # Development helper used with the `lucky watch` command.
+ # Reloads the browser when files are updated.
+ live_reload_connect_tag if LuckyEnv.development?
+ end
+ end
+end
diff --git a/fixtures/browser_src_template/expected/src/css/app.scss b/fixtures/browser_src_template/expected/src/css/app.scss
new file mode 100644
index 00000000..68b60cc9
--- /dev/null
+++ b/fixtures/browser_src_template/expected/src/css/app.scss
@@ -0,0 +1,66 @@
+// Lucky generates 3 folders to help you organize your CSS:
+//
+// - src/css/variables # Files for colors, spacing, etc.
+// - src/css/mixins # Put your mixin functions in files here
+// - src/css/components # CSS for your components
+//
+// Remember to import your new CSS files or they won't be loaded:
+//
+// @import "./variables/colors" # Imports the file in src/css/variables/_colors.scss
+//
+// Note: importing with `~` tells webpack to look in the installed npm packages
+// https://stackoverflow.com/questions/39535760/what-does-a-tilde-in-a-css-url-do
+
+@import 'modern-normalize/modern-normalize.css';
+// Add your own components and import them like this:
+//
+// @import "components/my_new_component";
+
+// Default Lucky styles.
+// Delete these when you're ready to bring in your own CSS.
+body {
+ font-family: system-ui, BlinkMacSystemFont, -apple-system, Segoe UI,
+ Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue,
+ sans-serif;
+ margin: 0 auto;
+ max-width: 800px;
+ padding: 20px 40px;
+}
+
+label, input {
+ display: flex;
+}
+
+label {
+ font-weight: 500;
+}
+
+[type='color'],
+[type='date'],
+[type='datetime'],
+[type='datetime-local'],
+[type='email'],
+[type='month'],
+[type='number'],
+[type='password'],
+[type='search'],
+[type='tel'],
+[type='text'],
+[type='time'],
+[type='url'],
+[type='week'],
+input:not([type]),
+textarea {
+ border-radius: 3px;
+ border: 1px solid #bbb;
+ margin: 7px 0 14px 0;
+ max-width: 400px;
+ padding: 8px 6px;
+ width: 100%;
+}
+
+[type='submit'] {
+ font-weight: 900;
+ margin: 9px 0;
+ padding: 6px 9px;
+}
diff --git a/fixtures/browser_src_template/expected/src/emails/.keep b/fixtures/browser_src_template/expected/src/emails/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/browser_src_template/expected/src/js/app.js b/fixtures/browser_src_template/expected/src/js/app.js
new file mode 100644
index 00000000..5722d9d8
--- /dev/null
+++ b/fixtures/browser_src_template/expected/src/js/app.js
@@ -0,0 +1,6 @@
+/* eslint no-console:0 */
+
+// Rails Unobtrusive JavaScript (UJS) is *required* for links in Lucky that use DELETE, POST and PUT.
+// Though it says "Rails" it actually works with any framework.
+require("@rails/ujs").start();
+
diff --git a/fixtures/browser_src_template/expected/src/models/mixins/.keep b/fixtures/browser_src_template/expected/src/models/mixins/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/browser_src_template/expected/src/operations/.keep b/fixtures/browser_src_template/expected/src/operations/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/browser_src_template/expected/src/operations/mixins/.keep b/fixtures/browser_src_template/expected/src/operations/mixins/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/browser_src_template/expected/src/pages/errors/show_page.cr b/fixtures/browser_src_template/expected/src/pages/errors/show_page.cr
new file mode 100644
index 00000000..e7636de9
--- /dev/null
+++ b/fixtures/browser_src_template/expected/src/pages/errors/show_page.cr
@@ -0,0 +1,93 @@
+class Errors::ShowPage
+ include Lucky::HTMLPage
+
+ needs message : String
+ needs status_code : Int32
+
+ def render
+ html_doctype
+ html lang: "en" do
+ head do
+ utf8_charset
+ title "Something went wrong"
+ load_lato_font
+ normalize_styles
+ error_page_styles
+ end
+
+ body do
+ div class: "container" do
+ h2 status_code, class: "status-code"
+ h1 message, class: "message"
+
+ ul class: "helpful-links" do
+ li do
+ a "Try heading back to home", href: "/", class: "helpful-link"
+ end
+ end
+ end
+ end
+ end
+ end
+
+ def load_lato_font
+ css_link "https://fonts.googleapis.com/css?family=Lato"
+ end
+
+ def normalize_styles
+ style <<-CSS
+ /*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}template{display:none}[hidden]{display:none}
+ CSS
+ end
+
+ def error_page_styles
+ style <<-CSS
+ body {
+ background-color: #f5f5f5;
+ color: #000;
+ font-family: 'Lato', sans-serif;
+ padding-top: 100px;
+ }
+
+ .helpful-links {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+ }
+
+ .helpful-link {
+ color: #15A38B;
+ }
+
+ .status-code {
+ opacity: 0.4;
+ font-size: 26px;
+ font-weight: normal;
+ }
+
+ .message {
+ font-size: 34px;
+ line-height: 56px;
+ font-weight: normal;
+ }
+
+ .container {
+ margin: 0 auto;
+ max-width: 450px;
+ padding: 55px;
+ }
+
+ @media only screen and (max-width: 500px) {
+ .status-code {
+ font-size: 18px;
+ }
+
+ .message {
+ font-size: 26px;
+ line-height: 40px;
+ margin: 20px 0 35px 0;
+ }
+ }
+ CSS
+ end
+end
diff --git a/fixtures/browser_src_template/expected/src/pages/main_layout.cr b/fixtures/browser_src_template/expected/src/pages/main_layout.cr
new file mode 100644
index 00000000..40f7c5ce
--- /dev/null
+++ b/fixtures/browser_src_template/expected/src/pages/main_layout.cr
@@ -0,0 +1,27 @@
+abstract class MainLayout
+ include Lucky::HTMLPage
+
+ abstract def content
+ abstract def page_title
+
+ # The default page title. It is passed to `Shared::LayoutHead`.
+ #
+ # Add a `page_title` method to pages to override it. You can also remove
+ # This method so every page is required to have its own page title.
+ def page_title
+ "Welcome"
+ end
+
+ def render
+ html_doctype
+
+ html lang: "en" do
+ mount Shared::LayoutHead, page_title: page_title
+
+ body do
+ mount Shared::FlashMessages, context.flash
+ content
+ end
+ end
+ end
+end
diff --git a/fixtures/browser_src_template/expected/webpack.mix.js b/fixtures/browser_src_template/expected/webpack.mix.js
new file mode 100644
index 00000000..0c3ecc9b
--- /dev/null
+++ b/fixtures/browser_src_template/expected/webpack.mix.js
@@ -0,0 +1,113 @@
+/*
+ | Mix Asset Management
+ |
+ | Mix provides a clean, fluent API for defining some Webpack build steps
+ | for your application.
+ |
+ | Docs: https://github.com/JeffreyWay/laravel-mix/tree/master/docs#readme
+ */
+
+let mix = require("laravel-mix");
+let plugins = [];
+
+// Customize the notifier to be less noisy
+let WebpackNotifierPlugin = require('webpack-notifier');
+let webpackNotifier = new WebpackNotifierPlugin({
+ alwaysNotify: false,
+ skipFirstNotification: true
+})
+plugins.push(webpackNotifier)
+
+// Compress static assets in production
+if (mix.inProduction()) {
+ let CompressionWepackPlugin = require('compression-webpack-plugin');
+ let gzipCompression = new CompressionWepackPlugin({
+ compressionOptions: { level: 9 },
+ test: /\.js$|\.css$|\.html$|\.svg$/
+ })
+ plugins.push(gzipCompression)
+
+ // Add additional compression plugins here.
+ // For example if you want to add Brotli compression:
+ //
+ // let brotliCompression = new CompressionWepackPlugin({
+ // compressionOptions: { level: 11 },
+ // filename: '[path].br[query]',
+ // algorithm: 'brotliCompress',
+ // test: /\.js$|\.css$|\.html$|\.svg$/
+ // })
+ // plugins.push(brotliCompression)
+}
+
+mix
+ // Set public path so manifest gets output here
+ .setPublicPath("public")
+ // JS entry file. Supports Vue, and uses Babel
+ //
+ // More info and options (like React support) here:
+ // https://github.com/JeffreyWay/laravel-mix/blob/master/docs/mixjs.md
+ .js("src/js/app.js", "js")
+ // SASS entry file. Uses autoprefixer automatically.
+ .sass("src/css/app.scss", "css")
+ // Customize postCSS:
+ // https://github.com/JeffreyWay/laravel-mix/blob/master/docs/css-preprocessors.md#postcss-plugins
+ .options({
+ // If you want to process images, change this to true and add options from
+ // https://github.com/tcoopman/image-webpack-loader
+ imgLoaderOptions: { enabled: false },
+ // Stops Mix from clearing the console when compilation succeeds
+ clearConsole: false
+ })
+ // Add assets to the manifest
+ .version(["public/assets"])
+ // Reduce noise in Webpack output
+ .webpackConfig({
+ stats: "errors-only",
+ plugins: plugins,
+ watchOptions: {
+ ignored: /node_modules/
+ }
+ })
+ // Disable default Mix notifications because we're using our own notifier
+ .disableNotifications()
+
+// Full API
+// Docs: https://github.com/JeffreyWay/laravel-mix/tree/master/docs#readme
+//
+// mix.js(src, output);
+// mix.react(src, output); <-- Identical to mix.js(), but registers React Babel compilation.
+// mix.preact(src, output); <-- Identical to mix.js(), but registers Preact compilation.
+// mix.coffee(src, output); <-- Identical to mix.js(), but registers CoffeeScript compilation.
+// mix.ts(src, output); <-- TypeScript support. Requires tsconfig.json to exist in the same folder as webpack.mix.js
+// mix.extract(vendorLibs);
+// mix.sass(src, output);
+// mix.less(src, output);
+// mix.stylus(src, output);
+// mix.postCss(src, output, [require('postcss-some-plugin')()]);
+// mix.browserSync('my-site.test');
+// mix.combine(files, destination);
+// mix.babel(files, destination); <-- Identical to mix.combine(), but also includes Babel compilation.
+// mix.copy(from, to);
+// mix.copyDirectory(fromDir, toDir);
+// mix.minify(file);
+// mix.sourceMaps(); // Enable sourcemaps
+// mix.version(); // Enable versioning.
+// mix.disableNotifications();
+// mix.setPublicPath('path/to/public');
+// mix.setResourceRoot('prefix/for/resource/locators');
+// mix.autoload({}); <-- Will be passed to Webpack's ProvidePlugin.
+// mix.webpackConfig({}); <-- Override webpack.config.js, without editing the file directly.
+// mix.babelConfig({}); <-- Merge extra Babel configuration (plugins, etc.) with Mix's default.
+// mix.then(function () {}) <-- Will be triggered each time Webpack finishes building.
+// mix.when(condition, function (mix) {}) <-- Call function if condition is true.
+// mix.override(function (webpackConfig) {}) <-- Will be triggered once the webpack config object has been fully generated by Mix.
+// mix.dump(); <-- Dump the generated webpack config object to the console.
+// mix.extend(name, handler) <-- Extend Mix's API with your own components.
+// mix.options({
+// extractVueStyles: false, // Extract .vue component styling to file, rather than inline.
+// globalVueStyles: file, // Variables file to be imported in every component.
+// processCssUrls: true, // Process/optimize relative stylesheet url()'s. Set to false, if you don't want them touched.
+// purifyCss: false, // Remove unused CSS selectors.
+// terser: {}, // Terser-specific options. https://github.com/webpack-contrib/terser-webpack-plugin#options
+// postCss: [] // Post-CSS options: https://github.com/postcss/postcss/blob/master/docs/plugins.md
+// });
diff --git a/fixtures/browser_src_template__generate_auth/expected/bs-config.js b/fixtures/browser_src_template__generate_auth/expected/bs-config.js
new file mode 100644
index 00000000..dbba89ce
--- /dev/null
+++ b/fixtures/browser_src_template__generate_auth/expected/bs-config.js
@@ -0,0 +1,26 @@
+/*
+ | Browser-sync config file
+ |
+ | For up-to-date information about the options:
+ | http://www.browsersync.io/docs/options/
+ |
+ */
+
+module.exports = {
+ snippetOptions: {
+ rule: {
+ match: /<\/head>/i,
+ fn: function (snippet, match) {
+ return snippet + match;
+ }
+ }
+ },
+ files: ["public/css/**/*.css", "public/js/**/*.js"],
+ watchEvents: ["change"],
+ open: false,
+ browser: "default",
+ ghostMode: false,
+ ui: false,
+ online: false,
+ logConnections: false
+};
diff --git a/fixtures/browser_src_template__generate_auth/expected/config/html_page.cr b/fixtures/browser_src_template__generate_auth/expected/config/html_page.cr
new file mode 100644
index 00000000..dca168e5
--- /dev/null
+++ b/fixtures/browser_src_template__generate_auth/expected/config/html_page.cr
@@ -0,0 +1,3 @@
+Lucky::HTMLPage.configure do |settings|
+ settings.render_component_comments = !LuckyEnv.production?
+end
diff --git a/fixtures/browser_src_template__generate_auth/expected/db/migrations/.keep b/fixtures/browser_src_template__generate_auth/expected/db/migrations/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/browser_src_template__generate_auth/expected/package.json b/fixtures/browser_src_template__generate_auth/expected/package.json
new file mode 100644
index 00000000..fd6b12fd
--- /dev/null
+++ b/fixtures/browser_src_template__generate_auth/expected/package.json
@@ -0,0 +1,24 @@
+{
+ "license": "UNLICENSED",
+ "private": true,
+ "dependencies": {
+ "@rails/ujs": "^6.0.0",
+ "modern-normalize": "^1.1.0"
+ },
+ "scripts": {
+ "heroku-postbuild": "yarn prod",
+ "dev": "yarn run mix",
+ "watch": "yarn run mix watch",
+ "prod": "yarn run mix --production"
+ },
+ "devDependencies": {
+ "@babel/compat-data": "^7.9.0",
+ "browser-sync": "^2.26.7",
+ "compression-webpack-plugin": "^7.0.0",
+ "laravel-mix": "^6.0.0",
+ "postcss": "^8.1.0",
+ "resolve-url-loader": "^3.1.1",
+ "sass": "^1.26.10",
+ "sass-loader": "^10.0.2"
+ }
+}
diff --git a/fixtures/browser_src_template__generate_auth/expected/public/assets/images/.keep b/fixtures/browser_src_template__generate_auth/expected/public/assets/images/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/browser_src_template__generate_auth/expected/public/favicon.ico b/fixtures/browser_src_template__generate_auth/expected/public/favicon.ico
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/browser_src_template__generate_auth/expected/public/mix-manifest.json b/fixtures/browser_src_template__generate_auth/expected/public/mix-manifest.json
new file mode 100644
index 00000000..d70320ad
--- /dev/null
+++ b/fixtures/browser_src_template__generate_auth/expected/public/mix-manifest.json
@@ -0,0 +1,4 @@
+{
+ "/css/app.css": "/css/app.css",
+ "/js/app.js": "/js/app.js"
+}
diff --git a/fixtures/browser_src_template__generate_auth/expected/public/robots.txt b/fixtures/browser_src_template__generate_auth/expected/public/robots.txt
new file mode 100644
index 00000000..12009050
--- /dev/null
+++ b/fixtures/browser_src_template__generate_auth/expected/public/robots.txt
@@ -0,0 +1,4 @@
+# Learn more about robots.txt: https://www.robotstxt.org/robotstxt.html
+User-agent: *
+# 'Disallow' with an empty value allows all paths to be crawled
+Disallow:
diff --git a/fixtures/browser_src_template__generate_auth/expected/spec/flows/.keep b/fixtures/browser_src_template__generate_auth/expected/spec/flows/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/browser_src_template__generate_auth/expected/spec/setup/.keep b/fixtures/browser_src_template__generate_auth/expected/spec/setup/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/browser_src_template__generate_auth/expected/spec/setup/configure_lucky_flow.cr b/fixtures/browser_src_template__generate_auth/expected/spec/setup/configure_lucky_flow.cr
new file mode 100644
index 00000000..504a3d34
--- /dev/null
+++ b/fixtures/browser_src_template__generate_auth/expected/spec/setup/configure_lucky_flow.cr
@@ -0,0 +1,37 @@
+# For more detailed documentation, visit
+# https://luckyframework.org/guides/testing/html-and-interactivity
+
+LuckyFlow.configure do |settings|
+ settings.stop_retrying_after = 200.milliseconds
+ settings.base_uri = Lucky::RouteHelper.settings.base_uri
+
+ # LuckyFlow will install the chromedriver for you located in
+ # ~./webdrivers/. Uncomment this to point to a specific driver
+ # settings.driver_path = "/path/to/specific/chromedriver"
+end
+
+# By default, LuckyFlow is set in "headless" mode (no browser window shown).
+# Uncomment this to enable running `LuckyFlow` in a Google Chrome window instead.
+# Be sure to disable for CI.
+#
+# LuckyFlow.default_driver = "chrome"
+
+# LuckyFlow uses a registry for each driver. By default, chrome, and headless_chrome
+# are available. If you'd like to register your own custom driver, you can register
+# it here.
+#
+# LuckyFlow::Registry.register :firefox do
+# # add your custom driver here
+# end
+
+# Setup specs to allow you to change the driver on the fly
+# per spec by setting a tag on specific specs. Requires the
+# driver to be registered through `LuckyFlow::Registry` first.
+#
+# ```
+# it "uses headless_chrome" do
+# end
+# it "uses webless", tags: "webless" do
+# end
+# ```
+LuckyFlow::Spec.setup
diff --git a/fixtures/browser_src_template__generate_auth/expected/spec/support/.keep b/fixtures/browser_src_template__generate_auth/expected/spec/support/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/browser_src_template__generate_auth/expected/spec/support/factories/.keep b/fixtures/browser_src_template__generate_auth/expected/spec/support/factories/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/browser_src_template__generate_auth/expected/spec/support/flows/base_flow.cr b/fixtures/browser_src_template__generate_auth/expected/spec/support/flows/base_flow.cr
new file mode 100644
index 00000000..93709b25
--- /dev/null
+++ b/fixtures/browser_src_template__generate_auth/expected/spec/support/flows/base_flow.cr
@@ -0,0 +1,3 @@
+# Add methods that all or most Flows need to share
+class BaseFlow < LuckyFlow
+end
diff --git a/fixtures/browser_src_template__generate_auth/expected/src/actions/browser_action.cr b/fixtures/browser_src_template__generate_auth/expected/src/actions/browser_action.cr
new file mode 100644
index 00000000..a72377f4
--- /dev/null
+++ b/fixtures/browser_src_template__generate_auth/expected/src/actions/browser_action.cr
@@ -0,0 +1,18 @@
+abstract class BrowserAction < Lucky::Action
+ include Lucky::ProtectFromForgery
+
+ # By default all actions are required to use underscores.
+ # Add `include Lucky::SkipRouteStyleCheck` to your actions if you wish to ignore this check for specific routes.
+ include Lucky::EnforceUnderscoredRoute
+
+ # This module disables Google FLoC by setting the
+ # [Permissions-Policy](https://github.com/WICG/floc) HTTP header to `interest-cohort=()`.
+ #
+ # This header is a part of Google's Federated Learning of Cohorts (FLoC) which is used
+ # to track browsing history instead of using 3rd-party cookies.
+ #
+ # Remove this include if you want to use the FLoC tracking.
+ include Lucky::SecureHeaders::DisableFLoC
+
+ accepted_formats [:html, :json], default: :html
+end
diff --git a/fixtures/browser_src_template__generate_auth/expected/src/components/.keep b/fixtures/browser_src_template__generate_auth/expected/src/components/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/browser_src_template__generate_auth/expected/src/components/base_component.cr b/fixtures/browser_src_template__generate_auth/expected/src/components/base_component.cr
new file mode 100644
index 00000000..c9829b48
--- /dev/null
+++ b/fixtures/browser_src_template__generate_auth/expected/src/components/base_component.cr
@@ -0,0 +1,2 @@
+abstract class BaseComponent < Lucky::BaseComponent
+end
diff --git a/fixtures/browser_src_template__generate_auth/expected/src/components/shared/field.cr b/fixtures/browser_src_template__generate_auth/expected/src/components/shared/field.cr
new file mode 100644
index 00000000..5c32e8a8
--- /dev/null
+++ b/fixtures/browser_src_template__generate_auth/expected/src/components/shared/field.cr
@@ -0,0 +1,57 @@
+# This component is used to make it easier to render the same fields styles
+# throughout your app.
+#
+# Extensive documentation at: https://luckyframework.org/guides/frontend/html-forms#shared-components
+#
+# ## Basic usage:
+#
+# # Renders a text input by default and will guess the label name "Name"
+# mount Shared::Field, op.name
+# # Call any of the input methods on the block
+# mount Shared::Field, op.email, &.email_input
+# # Add other HTML attributes
+# mount Shared::Field, op.email, &.email_input(autofocus: "true")
+# # Pass an explicit label name
+# mount Shared::Field, attribute: op.username, label_text: "Your username"
+#
+# ## Customization
+#
+# You can customize this component so that fields render like you expect.
+# For example, you might wrap it in a div with a "field-wrapper" class.
+#
+# div class: "field-wrapper"
+# label_for field
+# yield field
+# mount Shared::FieldErrors, field
+# end
+#
+# You may also want to have more components if your fields look
+# different in different parts of your app, e.g. `CompactField` or
+# `InlineTextField`
+class Shared::Field(T) < BaseComponent
+ # Raises a helpful error if component receives an unpermitted attribute
+ include Lucky::CatchUnpermittedAttribute
+
+ needs attribute : Avram::PermittedAttribute(T)
+ needs label_text : String?
+
+ def render(&)
+ label_for attribute, label_text
+
+ # You can add more default options here. For example:
+ #
+ # tag_defaults field: attribute, class: "input"
+ #
+ # Will add the class "input" to the generated HTML.
+ tag_defaults field: attribute do |tag_builder|
+ yield tag_builder
+ end
+
+ mount Shared::FieldErrors, attribute
+ end
+
+ # Use a text_input by default
+ def render
+ render &.text_input
+ end
+end
diff --git a/fixtures/browser_src_template__generate_auth/expected/src/components/shared/field_errors.cr b/fixtures/browser_src_template__generate_auth/expected/src/components/shared/field_errors.cr
new file mode 100644
index 00000000..3f2937a0
--- /dev/null
+++ b/fixtures/browser_src_template__generate_auth/expected/src/components/shared/field_errors.cr
@@ -0,0 +1,16 @@
+class Shared::FieldErrors(T) < BaseComponent
+ needs attribute : Avram::PermittedAttribute(T)
+
+ # Customize the markup and styles to match your application
+ def render
+ unless attribute.valid?
+ div class: "error" do
+ text "#{label_text} #{attribute.errors.first}"
+ end
+ end
+ end
+
+ def label_text : String
+ Wordsmith::Inflector.humanize(attribute.name.to_s)
+ end
+end
diff --git a/fixtures/browser_src_template__generate_auth/expected/src/components/shared/flash_messages.cr b/fixtures/browser_src_template__generate_auth/expected/src/components/shared/flash_messages.cr
new file mode 100644
index 00000000..bc44440d
--- /dev/null
+++ b/fixtures/browser_src_template__generate_auth/expected/src/components/shared/flash_messages.cr
@@ -0,0 +1,11 @@
+class Shared::FlashMessages < BaseComponent
+ needs flash : Lucky::FlashStore
+
+ def render
+ flash.each do |flash_type, flash_message|
+ div class: "flash-#{flash_type}", flow_id: "flash" do
+ text flash_message
+ end
+ end
+ end
+end
diff --git a/fixtures/browser_src_template__generate_auth/expected/src/components/shared/layout_head.cr b/fixtures/browser_src_template__generate_auth/expected/src/components/shared/layout_head.cr
new file mode 100644
index 00000000..5a053315
--- /dev/null
+++ b/fixtures/browser_src_template__generate_auth/expected/src/components/shared/layout_head.cr
@@ -0,0 +1,18 @@
+class Shared::LayoutHead < BaseComponent
+ needs page_title : String
+
+ def render
+ head do
+ utf8_charset
+ title "My App - #{@page_title}"
+ css_link asset("css/app.css")
+ js_link asset("js/app.js"), defer: "true"
+ csrf_meta_tags
+ responsive_meta_tag
+
+ # Development helper used with the `lucky watch` command.
+ # Reloads the browser when files are updated.
+ live_reload_connect_tag if LuckyEnv.development?
+ end
+ end
+end
diff --git a/fixtures/browser_src_template__generate_auth/expected/src/css/app.scss b/fixtures/browser_src_template__generate_auth/expected/src/css/app.scss
new file mode 100644
index 00000000..68b60cc9
--- /dev/null
+++ b/fixtures/browser_src_template__generate_auth/expected/src/css/app.scss
@@ -0,0 +1,66 @@
+// Lucky generates 3 folders to help you organize your CSS:
+//
+// - src/css/variables # Files for colors, spacing, etc.
+// - src/css/mixins # Put your mixin functions in files here
+// - src/css/components # CSS for your components
+//
+// Remember to import your new CSS files or they won't be loaded:
+//
+// @import "./variables/colors" # Imports the file in src/css/variables/_colors.scss
+//
+// Note: importing with `~` tells webpack to look in the installed npm packages
+// https://stackoverflow.com/questions/39535760/what-does-a-tilde-in-a-css-url-do
+
+@import 'modern-normalize/modern-normalize.css';
+// Add your own components and import them like this:
+//
+// @import "components/my_new_component";
+
+// Default Lucky styles.
+// Delete these when you're ready to bring in your own CSS.
+body {
+ font-family: system-ui, BlinkMacSystemFont, -apple-system, Segoe UI,
+ Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue,
+ sans-serif;
+ margin: 0 auto;
+ max-width: 800px;
+ padding: 20px 40px;
+}
+
+label, input {
+ display: flex;
+}
+
+label {
+ font-weight: 500;
+}
+
+[type='color'],
+[type='date'],
+[type='datetime'],
+[type='datetime-local'],
+[type='email'],
+[type='month'],
+[type='number'],
+[type='password'],
+[type='search'],
+[type='tel'],
+[type='text'],
+[type='time'],
+[type='url'],
+[type='week'],
+input:not([type]),
+textarea {
+ border-radius: 3px;
+ border: 1px solid #bbb;
+ margin: 7px 0 14px 0;
+ max-width: 400px;
+ padding: 8px 6px;
+ width: 100%;
+}
+
+[type='submit'] {
+ font-weight: 900;
+ margin: 9px 0;
+ padding: 6px 9px;
+}
diff --git a/fixtures/browser_src_template__generate_auth/expected/src/emails/.keep b/fixtures/browser_src_template__generate_auth/expected/src/emails/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/browser_src_template__generate_auth/expected/src/js/app.js b/fixtures/browser_src_template__generate_auth/expected/src/js/app.js
new file mode 100644
index 00000000..5722d9d8
--- /dev/null
+++ b/fixtures/browser_src_template__generate_auth/expected/src/js/app.js
@@ -0,0 +1,6 @@
+/* eslint no-console:0 */
+
+// Rails Unobtrusive JavaScript (UJS) is *required* for links in Lucky that use DELETE, POST and PUT.
+// Though it says "Rails" it actually works with any framework.
+require("@rails/ujs").start();
+
diff --git a/fixtures/browser_src_template__generate_auth/expected/src/models/mixins/.keep b/fixtures/browser_src_template__generate_auth/expected/src/models/mixins/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/browser_src_template__generate_auth/expected/src/operations/.keep b/fixtures/browser_src_template__generate_auth/expected/src/operations/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/browser_src_template__generate_auth/expected/src/operations/mixins/.keep b/fixtures/browser_src_template__generate_auth/expected/src/operations/mixins/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/browser_src_template__generate_auth/expected/src/pages/errors/show_page.cr b/fixtures/browser_src_template__generate_auth/expected/src/pages/errors/show_page.cr
new file mode 100644
index 00000000..e7636de9
--- /dev/null
+++ b/fixtures/browser_src_template__generate_auth/expected/src/pages/errors/show_page.cr
@@ -0,0 +1,93 @@
+class Errors::ShowPage
+ include Lucky::HTMLPage
+
+ needs message : String
+ needs status_code : Int32
+
+ def render
+ html_doctype
+ html lang: "en" do
+ head do
+ utf8_charset
+ title "Something went wrong"
+ load_lato_font
+ normalize_styles
+ error_page_styles
+ end
+
+ body do
+ div class: "container" do
+ h2 status_code, class: "status-code"
+ h1 message, class: "message"
+
+ ul class: "helpful-links" do
+ li do
+ a "Try heading back to home", href: "/", class: "helpful-link"
+ end
+ end
+ end
+ end
+ end
+ end
+
+ def load_lato_font
+ css_link "https://fonts.googleapis.com/css?family=Lato"
+ end
+
+ def normalize_styles
+ style <<-CSS
+ /*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}template{display:none}[hidden]{display:none}
+ CSS
+ end
+
+ def error_page_styles
+ style <<-CSS
+ body {
+ background-color: #f5f5f5;
+ color: #000;
+ font-family: 'Lato', sans-serif;
+ padding-top: 100px;
+ }
+
+ .helpful-links {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+ }
+
+ .helpful-link {
+ color: #15A38B;
+ }
+
+ .status-code {
+ opacity: 0.4;
+ font-size: 26px;
+ font-weight: normal;
+ }
+
+ .message {
+ font-size: 34px;
+ line-height: 56px;
+ font-weight: normal;
+ }
+
+ .container {
+ margin: 0 auto;
+ max-width: 450px;
+ padding: 55px;
+ }
+
+ @media only screen and (max-width: 500px) {
+ .status-code {
+ font-size: 18px;
+ }
+
+ .message {
+ font-size: 26px;
+ line-height: 40px;
+ margin: 20px 0 35px 0;
+ }
+ }
+ CSS
+ end
+end
diff --git a/fixtures/browser_src_template__generate_auth/expected/src/pages/main_layout.cr b/fixtures/browser_src_template__generate_auth/expected/src/pages/main_layout.cr
new file mode 100644
index 00000000..40f7c5ce
--- /dev/null
+++ b/fixtures/browser_src_template__generate_auth/expected/src/pages/main_layout.cr
@@ -0,0 +1,27 @@
+abstract class MainLayout
+ include Lucky::HTMLPage
+
+ abstract def content
+ abstract def page_title
+
+ # The default page title. It is passed to `Shared::LayoutHead`.
+ #
+ # Add a `page_title` method to pages to override it. You can also remove
+ # This method so every page is required to have its own page title.
+ def page_title
+ "Welcome"
+ end
+
+ def render
+ html_doctype
+
+ html lang: "en" do
+ mount Shared::LayoutHead, page_title: page_title
+
+ body do
+ mount Shared::FlashMessages, context.flash
+ content
+ end
+ end
+ end
+end
diff --git a/fixtures/browser_src_template__generate_auth/expected/webpack.mix.js b/fixtures/browser_src_template__generate_auth/expected/webpack.mix.js
new file mode 100644
index 00000000..0c3ecc9b
--- /dev/null
+++ b/fixtures/browser_src_template__generate_auth/expected/webpack.mix.js
@@ -0,0 +1,113 @@
+/*
+ | Mix Asset Management
+ |
+ | Mix provides a clean, fluent API for defining some Webpack build steps
+ | for your application.
+ |
+ | Docs: https://github.com/JeffreyWay/laravel-mix/tree/master/docs#readme
+ */
+
+let mix = require("laravel-mix");
+let plugins = [];
+
+// Customize the notifier to be less noisy
+let WebpackNotifierPlugin = require('webpack-notifier');
+let webpackNotifier = new WebpackNotifierPlugin({
+ alwaysNotify: false,
+ skipFirstNotification: true
+})
+plugins.push(webpackNotifier)
+
+// Compress static assets in production
+if (mix.inProduction()) {
+ let CompressionWepackPlugin = require('compression-webpack-plugin');
+ let gzipCompression = new CompressionWepackPlugin({
+ compressionOptions: { level: 9 },
+ test: /\.js$|\.css$|\.html$|\.svg$/
+ })
+ plugins.push(gzipCompression)
+
+ // Add additional compression plugins here.
+ // For example if you want to add Brotli compression:
+ //
+ // let brotliCompression = new CompressionWepackPlugin({
+ // compressionOptions: { level: 11 },
+ // filename: '[path].br[query]',
+ // algorithm: 'brotliCompress',
+ // test: /\.js$|\.css$|\.html$|\.svg$/
+ // })
+ // plugins.push(brotliCompression)
+}
+
+mix
+ // Set public path so manifest gets output here
+ .setPublicPath("public")
+ // JS entry file. Supports Vue, and uses Babel
+ //
+ // More info and options (like React support) here:
+ // https://github.com/JeffreyWay/laravel-mix/blob/master/docs/mixjs.md
+ .js("src/js/app.js", "js")
+ // SASS entry file. Uses autoprefixer automatically.
+ .sass("src/css/app.scss", "css")
+ // Customize postCSS:
+ // https://github.com/JeffreyWay/laravel-mix/blob/master/docs/css-preprocessors.md#postcss-plugins
+ .options({
+ // If you want to process images, change this to true and add options from
+ // https://github.com/tcoopman/image-webpack-loader
+ imgLoaderOptions: { enabled: false },
+ // Stops Mix from clearing the console when compilation succeeds
+ clearConsole: false
+ })
+ // Add assets to the manifest
+ .version(["public/assets"])
+ // Reduce noise in Webpack output
+ .webpackConfig({
+ stats: "errors-only",
+ plugins: plugins,
+ watchOptions: {
+ ignored: /node_modules/
+ }
+ })
+ // Disable default Mix notifications because we're using our own notifier
+ .disableNotifications()
+
+// Full API
+// Docs: https://github.com/JeffreyWay/laravel-mix/tree/master/docs#readme
+//
+// mix.js(src, output);
+// mix.react(src, output); <-- Identical to mix.js(), but registers React Babel compilation.
+// mix.preact(src, output); <-- Identical to mix.js(), but registers Preact compilation.
+// mix.coffee(src, output); <-- Identical to mix.js(), but registers CoffeeScript compilation.
+// mix.ts(src, output); <-- TypeScript support. Requires tsconfig.json to exist in the same folder as webpack.mix.js
+// mix.extract(vendorLibs);
+// mix.sass(src, output);
+// mix.less(src, output);
+// mix.stylus(src, output);
+// mix.postCss(src, output, [require('postcss-some-plugin')()]);
+// mix.browserSync('my-site.test');
+// mix.combine(files, destination);
+// mix.babel(files, destination); <-- Identical to mix.combine(), but also includes Babel compilation.
+// mix.copy(from, to);
+// mix.copyDirectory(fromDir, toDir);
+// mix.minify(file);
+// mix.sourceMaps(); // Enable sourcemaps
+// mix.version(); // Enable versioning.
+// mix.disableNotifications();
+// mix.setPublicPath('path/to/public');
+// mix.setResourceRoot('prefix/for/resource/locators');
+// mix.autoload({}); <-- Will be passed to Webpack's ProvidePlugin.
+// mix.webpackConfig({}); <-- Override webpack.config.js, without editing the file directly.
+// mix.babelConfig({}); <-- Merge extra Babel configuration (plugins, etc.) with Mix's default.
+// mix.then(function () {}) <-- Will be triggered each time Webpack finishes building.
+// mix.when(condition, function (mix) {}) <-- Call function if condition is true.
+// mix.override(function (webpackConfig) {}) <-- Will be triggered once the webpack config object has been fully generated by Mix.
+// mix.dump(); <-- Dump the generated webpack config object to the console.
+// mix.extend(name, handler) <-- Extend Mix's API with your own components.
+// mix.options({
+// extractVueStyles: false, // Extract .vue component styling to file, rather than inline.
+// globalVueStyles: file, // Variables file to be imported in every component.
+// processCssUrls: true, // Process/optimize relative stylesheet url()'s. Set to false, if you don't want them touched.
+// purifyCss: false, // Remove unused CSS selectors.
+// terser: {}, // Terser-specific options. https://github.com/webpack-contrib/terser-webpack-plugin#options
+// postCss: [] // Post-CSS options: https://github.com/postcss/postcss/blob/master/docs/plugins.md
+// });
diff --git a/fixtures/shard_file_template/expected/shard.yml b/fixtures/shard_file_template/expected/shard.yml
new file mode 100644
index 00000000..8b3c3ec9
--- /dev/null
+++ b/fixtures/shard_file_template/expected/shard.yml
@@ -0,0 +1,39 @@
+---
+name: test-shard
+version: 0.1.0
+targets:
+ test-shard:
+ main: src/test-shard.cr
+crystal: '>= 1.6.2'
+dependencies:
+ lucky:
+ github: luckyframework/lucky
+ version: ~> 1.0.0
+ avram:
+ github: luckyframework/avram
+ version: ~> 1.0.0
+ carbon:
+ github: luckyframework/carbon
+ version: ~> 0.3.0
+ carbon_sendgrid_adapter:
+ github: luckyframework/carbon_sendgrid_adapter
+ version: ~> 0.3.0
+ lucky_env:
+ github: luckyframework/lucky_env
+ version: ~> 0.1.4
+ lucky_task:
+ github: luckyframework/lucky_task
+ version: ~> 0.1.1
+ authentic:
+ github: luckyframework/authentic
+ version: ~> 1.0.0
+ jwt:
+ github: crystal-community/jwt
+ version: ~> 1.6.0
+development_dependencies:
+ lucky_flow:
+ github: luckyframework/lucky_flow
+ version: ~> 0.9.0
+ lucky_sec_tester:
+ github: luckyframework/lucky_sec_tester
+ version: ~> 0.2.0
diff --git a/fixtures/shard_file_template__browser/expected/shard.yml b/fixtures/shard_file_template__browser/expected/shard.yml
new file mode 100644
index 00000000..f34293a8
--- /dev/null
+++ b/fixtures/shard_file_template__browser/expected/shard.yml
@@ -0,0 +1,30 @@
+---
+name: test-shard
+version: 0.1.0
+targets:
+ test-shard:
+ main: src/test-shard.cr
+crystal: '>= 1.6.2'
+dependencies:
+ lucky:
+ github: luckyframework/lucky
+ version: ~> 1.0.0
+ avram:
+ github: luckyframework/avram
+ version: ~> 1.0.0
+ carbon:
+ github: luckyframework/carbon
+ version: ~> 0.3.0
+ carbon_sendgrid_adapter:
+ github: luckyframework/carbon_sendgrid_adapter
+ version: ~> 0.3.0
+ lucky_env:
+ github: luckyframework/lucky_env
+ version: ~> 0.1.4
+ lucky_task:
+ github: luckyframework/lucky_task
+ version: ~> 0.1.1
+development_dependencies:
+ lucky_flow:
+ github: luckyframework/lucky_flow
+ version: ~> 0.9.0
diff --git a/fixtures/shard_file_template__generate_auth/expected/shard.yml b/fixtures/shard_file_template__generate_auth/expected/shard.yml
new file mode 100644
index 00000000..74574554
--- /dev/null
+++ b/fixtures/shard_file_template__generate_auth/expected/shard.yml
@@ -0,0 +1,33 @@
+---
+name: test-shard
+version: 0.1.0
+targets:
+ test-shard:
+ main: src/test-shard.cr
+crystal: '>= 1.6.2'
+dependencies:
+ lucky:
+ github: luckyframework/lucky
+ version: ~> 1.0.0
+ avram:
+ github: luckyframework/avram
+ version: ~> 1.0.0
+ carbon:
+ github: luckyframework/carbon
+ version: ~> 0.3.0
+ carbon_sendgrid_adapter:
+ github: luckyframework/carbon_sendgrid_adapter
+ version: ~> 0.3.0
+ lucky_env:
+ github: luckyframework/lucky_env
+ version: ~> 0.1.4
+ lucky_task:
+ github: luckyframework/lucky_task
+ version: ~> 0.1.1
+ authentic:
+ github: luckyframework/authentic
+ version: ~> 1.0.0
+ jwt:
+ github: crystal-community/jwt
+ version: ~> 1.6.0
+development_dependencies: {}
diff --git a/fixtures/shard_file_template__with_sec_tester/expected/shard.yml b/fixtures/shard_file_template__with_sec_tester/expected/shard.yml
new file mode 100644
index 00000000..6892e8e5
--- /dev/null
+++ b/fixtures/shard_file_template__with_sec_tester/expected/shard.yml
@@ -0,0 +1,30 @@
+---
+name: test-shard
+version: 0.1.0
+targets:
+ test-shard:
+ main: src/test-shard.cr
+crystal: '>= 1.6.2'
+dependencies:
+ lucky:
+ github: luckyframework/lucky
+ version: ~> 1.0.0
+ avram:
+ github: luckyframework/avram
+ version: ~> 1.0.0
+ carbon:
+ github: luckyframework/carbon
+ version: ~> 0.3.0
+ carbon_sendgrid_adapter:
+ github: luckyframework/carbon_sendgrid_adapter
+ version: ~> 0.3.0
+ lucky_env:
+ github: luckyframework/lucky_env
+ version: ~> 0.1.4
+ lucky_task:
+ github: luckyframework/lucky_task
+ version: ~> 0.1.1
+development_dependencies:
+ lucky_sec_tester:
+ github: luckyframework/lucky_sec_tester
+ version: ~> 0.2.0
diff --git a/fixtures/src_template/expected/.crystal-version b/fixtures/src_template/expected/.crystal-version
new file mode 100644
index 00000000..fdd3be6d
--- /dev/null
+++ b/fixtures/src_template/expected/.crystal-version
@@ -0,0 +1 @@
+1.6.2
diff --git a/fixtures/src_template/expected/.env b/fixtures/src_template/expected/.env
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template/expected/.github/workflows/ci.yml b/fixtures/src_template/expected/.github/workflows/ci.yml
new file mode 100644
index 00000000..84d0c1cf
--- /dev/null
+++ b/fixtures/src_template/expected/.github/workflows/ci.yml
@@ -0,0 +1,93 @@
+name: test-project CI
+
+on:
+ push:
+ branches: "*"
+ pull_request:
+ branches: "*"
+
+jobs:
+ check-format:
+ strategy:
+ fail-fast: false
+ matrix:
+ crystal_version:
+ - 1.6.2
+ experimental:
+ - false
+ runs-on: ubuntu-latest
+ continue-on-error: ${{ matrix.experimental }}
+ steps:
+ - uses: actions/checkout@v2
+ - name: Install Crystal
+ uses: crystal-lang/install-crystal@v1
+ with:
+ crystal: ${{ matrix.crystal_version }}
+ - name: Format
+ run: crystal tool format --check
+
+ specs:
+ strategy:
+ fail-fast: false
+ matrix:
+ crystal_version:
+ - 1.6.2
+ experimental:
+ - false
+ runs-on: ubuntu-latest
+ env:
+ LUCKY_ENV: test
+ DB_HOST: localhost
+ continue-on-error: ${{ matrix.experimental }}
+ services:
+ postgres:
+ image: postgres:12-alpine
+ env:
+ POSTGRES_PASSWORD: postgres
+ ports:
+ - 5432:5432
+ # Set health checks to wait until postgres has started
+ options: >-
+ --health-cmd pg_isready
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Install Crystal
+ uses: crystal-lang/install-crystal@v1
+ with:
+ crystal: ${{ matrix.crystal_version }}
+
+ - name: Set up Crystal cache
+ uses: actions/cache@v2
+ id: crystal-cache
+ with:
+ path: |
+ ~/.cache/crystal
+ lib
+ lucky_tasks
+ key: ${{ runner.os }}-crystal-${{ hashFiles('**/shard.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-crystal-
+
+ - name: Install shards
+ if: steps.crystal-cache.outputs.cache-hit != 'true'
+ run: shards check || shards install
+
+ - name: Install npm and Nexploit Repeater
+ run: |
+ sudo npm install -g @neuralegion/nexploit-cli --unsafe-perm=true
+ - name: Build lucky_tasks
+ if: steps.crystal-cache.outputs.cache-hit != 'true'
+ run: crystal build tasks.cr -o ./lucky_tasks
+
+ - name: Prepare database
+ run: |
+ ./lucky_tasks db.create
+ ./lucky_tasks db.migrate
+ ./lucky_tasks db.seed.required_data
+
+ - name: Run tests
+ run: crystal spec -Dwith_sec_tests
\ No newline at end of file
diff --git a/fixtures/src_template/expected/Procfile b/fixtures/src_template/expected/Procfile
new file mode 100644
index 00000000..e524d70d
--- /dev/null
+++ b/fixtures/src_template/expected/Procfile
@@ -0,0 +1,2 @@
+web: bin/app
+release: lucky db.migrate
diff --git a/fixtures/src_template/expected/Procfile.dev b/fixtures/src_template/expected/Procfile.dev
new file mode 100644
index 00000000..9cca7346
--- /dev/null
+++ b/fixtures/src_template/expected/Procfile.dev
@@ -0,0 +1,2 @@
+system_check: script/system_check && sleep 100000
+web: lucky watch
diff --git a/fixtures/src_template/expected/README.md b/fixtures/src_template/expected/README.md
new file mode 100644
index 00000000..da2d97cc
--- /dev/null
+++ b/fixtures/src_template/expected/README.md
@@ -0,0 +1,23 @@
+# test-project
+
+This is a project written using [Lucky](https://luckyframework.org). Enjoy!
+
+### Setting up the project
+
+1. [Install required dependencies](https://luckyframework.org/guides/getting-started/installing#install-required-dependencies)
+1. Update database settings in `config/database.cr`
+1. Run `script/setup`
+1. Run `lucky dev` to start the app
+
+### Using Docker for development
+
+1. [Install Docker](https://docs.docker.com/engine/install/)
+1. Run `docker compose up`
+
+The Docker container will boot all of the necessary components needed to run your Lucky application.
+To configure the container, update the `docker-compose.yml` file, and the `docker/development.dockerfile` file.
+
+
+### Learning Lucky
+
+Lucky uses the [Crystal](https://crystal-lang.org) programming language. You can learn about Lucky from the [Lucky Guides](https://luckyframework.org/guides/getting-started/why-lucky).
diff --git a/fixtures/src_template/expected/config/application.cr b/fixtures/src_template/expected/config/application.cr
new file mode 100644
index 00000000..c807149a
--- /dev/null
+++ b/fixtures/src_template/expected/config/application.cr
@@ -0,0 +1,24 @@
+# This file may be used for custom Application configurations.
+# It will be loaded before other config files.
+#
+# Read more on configuration:
+# https://luckyframework.org/guides/getting-started/configuration#configuring-your-own-code
+
+# Use this code as an example:
+#
+# ```
+# module Application
+# Habitat.create do
+# setting support_email : String
+# setting lock_with_basic_auth : Bool
+# end
+# end
+#
+# Application.configure do |settings|
+# settings.support_email = "support@myapp.io"
+# settings.lock_with_basic_auth = LuckyEnv.staging?
+# end
+#
+# # In your application, call
+# # `Application.settings.support_email` anywhere you need it.
+# ```
diff --git a/fixtures/src_template/expected/config/colors.cr b/fixtures/src_template/expected/config/colors.cr
new file mode 100644
index 00000000..761ae940
--- /dev/null
+++ b/fixtures/src_template/expected/config/colors.cr
@@ -0,0 +1,4 @@
+# This enables the color output when in development or test
+# Check out the Colorize docs for more information
+# https://crystal-lang.org/api/Colorize.html
+Colorize.enabled = LuckyEnv.development? || LuckyEnv.test?
diff --git a/fixtures/src_template/expected/config/cookies.cr b/fixtures/src_template/expected/config/cookies.cr
new file mode 100644
index 00000000..2d5055f2
--- /dev/null
+++ b/fixtures/src_template/expected/config/cookies.cr
@@ -0,0 +1,25 @@
+require "./server"
+
+Lucky::Session.configure do |settings|
+ settings.key = "_test_project_session"
+end
+
+Lucky::CookieJar.configure do |settings|
+ settings.on_set = ->(cookie : HTTP::Cookie) {
+ # If ForceSSLHandler is enabled, only send cookies over HTTPS
+ cookie.secure(Lucky::ForceSSLHandler.settings.enabled)
+
+ # By default, don't allow reading cookies with JavaScript
+ cookie.http_only(true)
+
+ # Restrict cookies to a first-party or same-site context
+ cookie.samesite(:lax)
+
+ # Set all cookies to the root path by default
+ cookie.path("/")
+
+ # You can set other defaults for cookies here. For example:
+ #
+ # cookie.expires(1.year.from_now).domain("mydomain.com")
+ }
+end
diff --git a/fixtures/src_template/expected/config/database.cr b/fixtures/src_template/expected/config/database.cr
new file mode 100644
index 00000000..f614299a
--- /dev/null
+++ b/fixtures/src_template/expected/config/database.cr
@@ -0,0 +1,29 @@
+database_name = "test_project_#{LuckyEnv.environment}"
+
+AppDatabase.configure do |settings|
+ if LuckyEnv.production?
+ settings.credentials = Avram::Credentials.parse(ENV["DATABASE_URL"])
+ else
+ settings.credentials = Avram::Credentials.parse?(ENV["DATABASE_URL"]?) || Avram::Credentials.new(
+ database: database_name,
+ hostname: ENV["DB_HOST"]? || "localhost",
+ port: ENV["DB_PORT"]?.try(&.to_i) || 5432,
+ # Some common usernames are "postgres", "root", or your system username (run 'whoami')
+ username: ENV["DB_USERNAME"]? || "postgres",
+ # Some Postgres installations require no password. Use "" if that is the case.
+ password: ENV["DB_PASSWORD"]? || "postgres"
+ )
+ end
+end
+
+Avram.configure do |settings|
+ settings.database_to_migrate = AppDatabase
+
+ # In production, allow lazy loading (N+1).
+ # In development and test, raise an error if you forget to preload associations
+ settings.lazy_load_enabled = LuckyEnv.production?
+
+ # Always parse `Time` values with these specific formats.
+ # Used for both database values, and datetime input fields.
+ # settings.time_formats << "%F"
+end
diff --git a/fixtures/src_template/expected/config/email.cr b/fixtures/src_template/expected/config/email.cr
new file mode 100644
index 00000000..7c875449
--- /dev/null
+++ b/fixtures/src_template/expected/config/email.cr
@@ -0,0 +1,26 @@
+require "carbon_sendgrid_adapter"
+
+BaseEmail.configure do |settings|
+ if LuckyEnv.production?
+ # If you don't need to send emails, set the adapter to DevAdapter instead:
+ #
+ # settings.adapter = Carbon::DevAdapter.new
+ #
+ # If you do need emails, get a key from SendGrid and set an ENV variable
+ send_grid_key = send_grid_key_from_env
+ settings.adapter = Carbon::SendGridAdapter.new(api_key: send_grid_key)
+ elsif LuckyEnv.development?
+ settings.adapter = Carbon::DevAdapter.new(print_emails: true)
+ else
+ settings.adapter = Carbon::DevAdapter.new
+ end
+end
+
+private def send_grid_key_from_env
+ ENV["SEND_GRID_KEY"]? || raise_missing_key_message
+end
+
+private def raise_missing_key_message
+ puts "Missing SEND_GRID_KEY. Set the SEND_GRID_KEY env variable to 'unused' if not sending emails, or set the SEND_GRID_KEY ENV var.".colorize.red
+ exit(1)
+end
diff --git a/fixtures/src_template/expected/config/env.cr b/fixtures/src_template/expected/config/env.cr
new file mode 100644
index 00000000..3f364072
--- /dev/null
+++ b/fixtures/src_template/expected/config/env.cr
@@ -0,0 +1,33 @@
+# Environments are managed using `LuckyEnv`. By default, development, production
+# and test are supported. See
+# https://luckyframework.org/guides/getting-started/configuration for details.
+#
+# The default environment is development unless the environment variable
+# LUCKY_ENV is set.
+#
+# Example:
+# ```
+# LuckyEnv.environment # => "development"
+# LuckyEnv.development? # => true
+# LuckyEnv.production? # => false
+# LuckyEnv.test? # => false
+# ```
+#
+# New environments can be added using the `LuckyEnv.add_env` macro.
+#
+# Example:
+# ```
+# LuckyEnv.add_env :staging
+# LuckyEnv.staging? # => false
+# ```
+#
+# To determine whether or not a `LuckyTask` is currently running, you can use
+# the `LuckyEnv.task?` predicate.
+#
+# Example:
+# ```
+# LuckyEnv.task? # => false
+# ```
+
+# Add a staging environment.
+# LuckyEnv.add_env :staging
diff --git a/fixtures/src_template/expected/config/error_handler.cr b/fixtures/src_template/expected/config/error_handler.cr
new file mode 100644
index 00000000..c6b736e3
--- /dev/null
+++ b/fixtures/src_template/expected/config/error_handler.cr
@@ -0,0 +1,3 @@
+Lucky::ErrorHandler.configure do |settings|
+ settings.show_debug_output = !LuckyEnv.production?
+end
diff --git a/fixtures/src_template/expected/config/log.cr b/fixtures/src_template/expected/config/log.cr
new file mode 100644
index 00000000..0e8d7636
--- /dev/null
+++ b/fixtures/src_template/expected/config/log.cr
@@ -0,0 +1,45 @@
+require "file_utils"
+
+if LuckyEnv.test?
+ # Logs to `tmp/test.log` so you can see what's happening without having
+ # a bunch of log output in your spec results.
+ FileUtils.mkdir_p("tmp")
+
+ backend = Log::IOBackend.new(File.new("tmp/test.log", mode: "w"))
+ backend.formatter = Lucky::PrettyLogFormatter.proc
+ Log.dexter.configure(:debug, backend)
+elsif LuckyEnv.production?
+ # Lucky uses JSON in production so logs can be searched more easily
+ #
+ # If you want logs like in develpoment use 'Lucky::PrettyLogFormatter.proc'.
+ backend = Log::IOBackend.new
+ backend.formatter = Dexter::JSONLogFormatter.proc
+ Log.dexter.configure(:info, backend)
+else
+ # Use a pretty formatter printing to STDOUT in development
+ backend = Log::IOBackend.new
+ backend.formatter = Lucky::PrettyLogFormatter.proc
+ Log.dexter.configure(:debug, backend)
+ DB::Log.level = :info
+end
+
+# Lucky only logs when before/after pipes halt by redirecting, or rendering a
+# response. Pipes that run without halting are not logged.
+#
+# If you want to log every pipe that runs, set the log level to ':info'
+Lucky::ContinuedPipeLog.dexter.configure(:none)
+
+# Lucky only logs failed queries by default.
+#
+# Set the log to ':info' to log all queries
+Avram::QueryLog.dexter.configure(:none)
+
+# Skip logging static assets requests in development
+Lucky::LogHandler.configure do |settings|
+ if LuckyEnv.development?
+ settings.skip_if = ->(context : HTTP::Server::Context) {
+ context.request.method.downcase == "get" &&
+ context.request.resource.starts_with?(/\/css\/|\/js\/|\/assets\/|\/favicon\.ico/)
+ }
+ end
+end
diff --git a/fixtures/src_template/expected/config/route_helper.cr b/fixtures/src_template/expected/config/route_helper.cr
new file mode 100644
index 00000000..ede1f328
--- /dev/null
+++ b/fixtures/src_template/expected/config/route_helper.cr
@@ -0,0 +1,10 @@
+# This is used when generating URLs for your application
+Lucky::RouteHelper.configure do |settings|
+ if LuckyEnv.production?
+ # Example: https://my_app.com
+ settings.base_uri = ENV.fetch("APP_DOMAIN")
+ else
+ # Set domain to the default host/port in development/test
+ settings.base_uri = "http://localhost:#{Lucky::ServerSettings.port}"
+ end
+end
diff --git a/fixtures/src_template/expected/config/server.cr b/fixtures/src_template/expected/config/server.cr
new file mode 100644
index 00000000..c72314be
--- /dev/null
+++ b/fixtures/src_template/expected/config/server.cr
@@ -0,0 +1,65 @@
+# Here is where you configure the Lucky server
+#
+# Look at config/route_helper.cr if you want to change the domain used when
+# generating links with `Action.url`.
+Lucky::Server.configure do |settings|
+ if LuckyEnv.production?
+ settings.secret_key_base = secret_key_from_env
+ settings.host = "0.0.0.0"
+ settings.port = ENV["PORT"].to_i
+ settings.gzip_enabled = true
+ # By default certain content types will be gzipped.
+ # For a full list look in
+ # https://github.com/luckyframework/lucky/blob/main/src/lucky/server.cr
+ # To add additional extensions do something like this:
+ # settings.gzip_content_types << "content/type"
+ else
+ settings.secret_key_base = "1234567890"
+ # Change host/port in config/watch.yml
+ # Alternatively, you can set the DEV_PORT env to set the port for local development
+ settings.host = Lucky::ServerSettings.host
+ settings.port = Lucky::ServerSettings.port
+ end
+
+ # By default Lucky will serve static assets in development and production.
+ #
+ # However you could use a CDN when in production like this:
+ #
+ # Lucky::Server.configure do |settings|
+ # if LuckyEnv.production?
+ # settings.asset_host = "https://mycdnhost.com"
+ # else
+ # settings.asset_host = ""
+ # end
+ # end
+ settings.asset_host = "" # Lucky will serve assets
+end
+
+Lucky::ForceSSLHandler.configure do |settings|
+ # To force SSL in production, uncomment the lines below.
+ # This will cause http requests to be redirected to https:
+ #
+ # settings.enabled = LuckyEnv.production?
+ # settings.strict_transport_security = {max_age: 1.year, include_subdomains: true}
+ #
+ # Or, leave it disabled:
+ settings.enabled = false
+end
+
+# Set a unique ID for each HTTP request.
+# To enable the request ID, uncomment the lines below.
+# You can set your own custom String, or use a random UUID.
+# Lucky::RequestIdHandler.configure do |settings|
+# settings.set_request_id = ->(context : HTTP::Server::Context) {
+# UUID.random.to_s
+# }
+# end
+
+private def secret_key_from_env
+ ENV["SECRET_KEY_BASE"]? || raise_missing_secret_key_in_production
+end
+
+private def raise_missing_secret_key_in_production
+ puts "Please set the SECRET_KEY_BASE environment variable. You can generate a secret key with 'lucky gen.secret_key'".colorize.red
+ exit(1)
+end
diff --git a/fixtures/src_template/expected/config/watch.yml b/fixtures/src_template/expected/config/watch.yml
new file mode 100644
index 00000000..3a59b410
--- /dev/null
+++ b/fixtures/src_template/expected/config/watch.yml
@@ -0,0 +1,3 @@
+host: 127.0.0.1
+port: 3000
+reload_port: 3001
diff --git a/fixtures/src_template/expected/db/migrations/.keep b/fixtures/src_template/expected/db/migrations/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template/expected/docker-compose.yml b/fixtures/src_template/expected/docker-compose.yml
new file mode 100644
index 00000000..d00779db
--- /dev/null
+++ b/fixtures/src_template/expected/docker-compose.yml
@@ -0,0 +1,45 @@
+version: "3.8"
+services:
+ lucky:
+ build:
+ context: .
+ dockerfile: docker/development.dockerfile
+ environment:
+ DATABASE_URL: postgres://lucky:password@postgres:5432/lucky
+ DEV_HOST: "0.0.0.0"
+ volumes:
+ - .:/app
+ - node_modules:/app/node_modules
+ - shards_lib:/app/lib
+ - app_bin:/app/bin
+ - build_cache:/root/.cache
+ depends_on:
+ - postgres
+ ports:
+ - 3000:3000 # This is the Lucky Server port
+ - 3001:3001 # This is the Lucky watcher reload port
+
+ entrypoint: ["docker/dev_entrypoint.sh"]
+
+ postgres:
+ image: postgres:14-alpine
+ environment:
+ POSTGRES_USER: lucky
+ POSTGRES_PASSWORD: password
+ POSTGRES_DB: lucky
+ volumes:
+ - postgres_data:/var/lib/postgresql/data
+ ports:
+ # The postgres database container is exposed on the host at port 6543 to
+ # allow connecting directly to it with postgres clients. The port differs
+ # from the postgres default to avoid conflict with existing postgres
+ # servers. Connect to a running postgres container with:
+ # postgres://lucky:password@localhost:6543/lucky
+ - 6543:5432
+
+volumes:
+ postgres_data:
+ node_modules:
+ shards_lib:
+ app_bin:
+ build_cache:
diff --git a/fixtures/src_template/expected/docker/dev_entrypoint.sh b/fixtures/src_template/expected/docker/dev_entrypoint.sh
new file mode 100755
index 00000000..f5aab877
--- /dev/null
+++ b/fixtures/src_template/expected/docker/dev_entrypoint.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+
+set -euo pipefail
+
+# This is the entrypoint script used for development docker workflows.
+# By default it will:
+# - Install dependencies.
+# - Run migrations.
+# - Start the dev server.
+# It also accepts any commands to be run instead.
+
+
+warnfail () {
+ echo "$@" >&2
+ exit 1
+}
+
+case ${1:-} in
+ "") # If no arguments are provided, start lucky dev server.
+ ;;
+
+ *) # If any arguments are provided, execute them instead.
+ exec "$@"
+esac
+
+if ! [ -d bin ] ; then
+ echo 'Creating bin directory'
+ mkdir bin
+fi
+if ! shards check ; then
+ echo 'Installing shards...'
+ shards install
+fi
+
+echo 'Waiting for postgres to be available...'
+./docker/wait-for-it.sh -q postgres:5432
+
+if ! psql -d "$DATABASE_URL" -c '\d migrations' > /dev/null ; then
+ echo 'Finishing database setup...'
+ lucky db.migrate
+fi
+
+echo 'Starting lucky dev server...'
+exec lucky dev
diff --git a/fixtures/src_template/expected/docker/development.dockerfile b/fixtures/src_template/expected/docker/development.dockerfile
new file mode 100644
index 00000000..11222751
--- /dev/null
+++ b/fixtures/src_template/expected/docker/development.dockerfile
@@ -0,0 +1,25 @@
+FROM crystallang/crystal:1.6.2
+
+# Install utilities required to make this Dockerfile run
+RUN apt-get update && \
+ apt-get install -y wget
+
+# Apt installs:
+# - Postgres cli tools are required for lucky-cli.
+# - tmux is required for the Overmind process manager.
+RUN apt-get update && \
+ apt-get install -y postgresql-client tmux && \
+ rm -rf /var/lib/apt/lists/*
+
+# Install lucky cli
+WORKDIR /lucky/cli
+RUN git clone https://github.com/luckyframework/lucky_cli . && \
+ git checkout v1.0.0 && \
+ shards build --without-development && \
+ cp bin/lucky /usr/bin
+
+WORKDIR /app
+ENV DATABASE_URL=postgres://postgres:postgres@host.docker.internal:5432/postgres
+EXPOSE 3000
+EXPOSE 3001
+
diff --git a/fixtures/src_template/expected/docker/wait-for-it.sh b/fixtures/src_template/expected/docker/wait-for-it.sh
new file mode 100755
index 00000000..06e0638c
--- /dev/null
+++ b/fixtures/src_template/expected/docker/wait-for-it.sh
@@ -0,0 +1,189 @@
+#!/usr/bin/bash
+#
+# Pulled from https://github.com/vishnubob/wait-for-it on 2022-02-28.
+# Licensed under the MIT license as of 81b1373f.
+#
+# Below this line, wait-for-it is the original work of the author.
+#
+# Use this script to test if a given TCP host/port are available
+
+WAITFORIT_cmdname=${0##*/}
+
+echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }
+
+usage()
+{
+ cat << USAGE >&2
+Usage:
+ $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args]
+ -h HOST | --host=HOST Host or IP under test
+ -p PORT | --port=PORT TCP port under test
+ Alternatively, you specify the host and port as host:port
+ -s | --strict Only execute subcommand if the test succeeds
+ -q | --quiet Don't output any status messages
+ -t TIMEOUT | --timeout=TIMEOUT
+ Timeout in seconds, zero for no timeout
+ -- COMMAND ARGS Execute command with args after the test finishes
+USAGE
+ exit 1
+}
+
+wait_for()
+{
+ if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
+ echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
+ else
+ echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout"
+ fi
+ WAITFORIT_start_ts=$(date +%s)
+ while :
+ do
+ if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then
+ nc -z $WAITFORIT_HOST $WAITFORIT_PORT
+ WAITFORIT_result=$?
+ else
+ (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1
+ WAITFORIT_result=$?
+ fi
+ if [[ $WAITFORIT_result -eq 0 ]]; then
+ WAITFORIT_end_ts=$(date +%s)
+ echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds"
+ break
+ fi
+ sleep 1
+ done
+ return $WAITFORIT_result
+}
+
+wait_for_wrapper()
+{
+ # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
+ if [[ $WAITFORIT_QUIET -eq 1 ]]; then
+ timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
+ else
+ timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
+ fi
+ WAITFORIT_PID=$!
+ trap "kill -INT -$WAITFORIT_PID" INT
+ wait $WAITFORIT_PID
+ WAITFORIT_RESULT=$?
+ if [[ $WAITFORIT_RESULT -ne 0 ]]; then
+ echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
+ fi
+ return $WAITFORIT_RESULT
+}
+
+# process arguments
+while [[ $# -gt 0 ]]
+do
+ case "$1" in
+ *:* )
+ WAITFORIT_hostport=(${1//:/ })
+ WAITFORIT_HOST=${WAITFORIT_hostport[0]}
+ WAITFORIT_PORT=${WAITFORIT_hostport[1]}
+ shift 1
+ ;;
+ --child)
+ WAITFORIT_CHILD=1
+ shift 1
+ ;;
+ -q | --quiet)
+ WAITFORIT_QUIET=1
+ shift 1
+ ;;
+ -s | --strict)
+ WAITFORIT_STRICT=1
+ shift 1
+ ;;
+ -h)
+ WAITFORIT_HOST="$2"
+ if [[ $WAITFORIT_HOST == "" ]]; then break; fi
+ shift 2
+ ;;
+ --host=*)
+ WAITFORIT_HOST="${1#*=}"
+ shift 1
+ ;;
+ -p)
+ WAITFORIT_PORT="$2"
+ if [[ $WAITFORIT_PORT == "" ]]; then break; fi
+ shift 2
+ ;;
+ --port=*)
+ WAITFORIT_PORT="${1#*=}"
+ shift 1
+ ;;
+ -t)
+ WAITFORIT_TIMEOUT="$2"
+ if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi
+ shift 2
+ ;;
+ --timeout=*)
+ WAITFORIT_TIMEOUT="${1#*=}"
+ shift 1
+ ;;
+ --)
+ shift
+ WAITFORIT_CLI=("$@")
+ break
+ ;;
+ --help)
+ usage
+ ;;
+ *)
+ echoerr "Unknown argument: $1"
+ usage
+ ;;
+ esac
+done
+
+if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then
+ echoerr "Error: you need to provide a host and port to test."
+ usage
+fi
+
+WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15}
+WAITFORIT_STRICT=${WAITFORIT_STRICT:-0}
+WAITFORIT_CHILD=${WAITFORIT_CHILD:-0}
+WAITFORIT_QUIET=${WAITFORIT_QUIET:-0}
+
+# Check to see if timeout is from busybox?
+WAITFORIT_TIMEOUT_PATH=$(type -p timeout)
+WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH)
+
+WAITFORIT_BUSYTIMEFLAG=""
+if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then
+ WAITFORIT_ISBUSY=1
+ # Check if busybox timeout uses -t flag
+ # (recent Alpine versions don't support -t anymore)
+ if timeout &>/dev/stdout | grep -q -e '-t '; then
+ WAITFORIT_BUSYTIMEFLAG="-t"
+ fi
+else
+ WAITFORIT_ISBUSY=0
+fi
+
+if [[ $WAITFORIT_CHILD -gt 0 ]]; then
+ wait_for
+ WAITFORIT_RESULT=$?
+ exit $WAITFORIT_RESULT
+else
+ if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
+ wait_for_wrapper
+ WAITFORIT_RESULT=$?
+ else
+ wait_for
+ WAITFORIT_RESULT=$?
+ fi
+fi
+
+if [[ $WAITFORIT_CLI != "" ]]; then
+ if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then
+ echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess"
+ exit $WAITFORIT_RESULT
+ fi
+ exec "${WAITFORIT_CLI[@]}"
+else
+ exit $WAITFORIT_RESULT
+fi
+
diff --git a/fixtures/src_template/expected/script/helpers/function_helpers b/fixtures/src_template/expected/script/helpers/function_helpers
new file mode 100644
index 00000000..388fa67e
--- /dev/null
+++ b/fixtures/src_template/expected/script/helpers/function_helpers
@@ -0,0 +1,67 @@
+#!/usr/bin/env bash
+
+# This file contains a set of functions used as helpers
+# for various tasks. Read the examples for each one for
+# more information. Feel free to put any additional helper
+# functions you may need for your app
+
+
+# Returns true if the command $1 is not found
+# example:
+# if command_not_found "yarn"; then
+# echo "no yarn"
+# fi
+command_not_found() {
+ ! command -v $1 > /dev/null
+ return $?
+}
+
+# Returns true if the command $1 is not running
+# You must supply the full command to check as an argument
+# example:
+# if command_not_running "redis-cli ping"; then
+# print_error "Redis is not running"
+# fi
+command_not_running() {
+ $1
+ if [ $? -ne 0 ]; then
+ true
+ else
+ false
+ fi
+}
+
+# Returns true if the OS is macOS
+# example:
+# if is_mac; then
+# echo "do mac stuff"
+# fi
+is_mac() {
+ if [[ "$OSTYPE" == "darwin"* ]]; then
+ true
+ else
+ false
+ fi
+}
+
+# Returns true if the OS is linux based
+# example:
+# if is_linux; then
+# echo "do linux stuff"
+# fi
+is_linux() {
+ if [[ "$OSTYPE" == "linux"* ]]; then
+ true
+ else
+ false
+ fi
+}
+
+# Prints error and exit.
+# example:
+# print_error "Redis is not running. Run it with some_command"
+print_error() {
+ printf "${BOLD_RED_COLOR}There is a problem with your system setup:\n\n"
+ printf "${BOLD_RED_COLOR}$1 \n\n" | indent
+ exit 1
+}
diff --git a/fixtures/src_template/expected/script/helpers/text_helpers b/fixtures/src_template/expected/script/helpers/text_helpers
new file mode 100644
index 00000000..34b77a8c
--- /dev/null
+++ b/fixtures/src_template/expected/script/helpers/text_helpers
@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+
+# This file contains a set of functions used to format text,
+# and make printing text a little easier. Feel free to put
+# any additional functions you need for formatting your shell
+# output text.
+
+# Colors
+BOLD_RED_COLOR="\e[1m\e[31m"
+
+# Indents the text 2 spaces
+# example:
+# printf "Hello" | indent
+indent() {
+ while read LINE; do
+ echo " $LINE" || true
+ done
+}
+
+# Prints out an arrow to your custom notice
+# example:
+# notice "Installing new magic"
+notice() {
+ printf "\n▸ $1\n"
+}
+
+# Prints out a check mark and Done.
+# example:
+# print_done
+print_done() {
+ printf "✔ Done\n" | indent
+}
diff --git a/fixtures/src_template/expected/script/setup b/fixtures/src_template/expected/script/setup
new file mode 100755
index 00000000..5a97531e
--- /dev/null
+++ b/fixtures/src_template/expected/script/setup
@@ -0,0 +1,38 @@
+#!/usr/bin/env bash
+
+# Exit if any subcommand fails
+set -e
+set -o pipefail
+
+source script/helpers/text_helpers
+
+
+notice "Running System Check"
+./script/system_check
+print_done
+
+
+notice "Installing shards"
+shards install --ignore-crystal-version | indent
+
+if [ ! -f ".env" ]; then
+ notice "No .env found. Creating one."
+ touch .env
+ print_done
+fi
+
+notice "Creating the database"
+lucky db.create | indent
+
+notice "Verifying postgres connection"
+lucky db.verify_connection | indent
+
+notice "Migrating the database"
+lucky db.migrate | indent
+
+notice "Seeding the database with required and sample records"
+lucky db.seed.required_data | indent
+lucky db.seed.sample_data | indent
+
+print_done
+notice "Run 'lucky dev' to start the app"
diff --git a/fixtures/src_template/expected/script/system_check b/fixtures/src_template/expected/script/system_check
new file mode 100755
index 00000000..eea2533a
--- /dev/null
+++ b/fixtures/src_template/expected/script/system_check
@@ -0,0 +1,34 @@
+#!/usr/bin/env bash
+
+source script/helpers/text_helpers
+source script/helpers/function_helpers
+
+# Use this script to check the system for required tools and process that your app needs.
+# A few helper functions are provided to make writing bash a little easier. See the
+# script/helpers/function_helpers file for more examples.
+#
+# A few examples you might use here:
+# * 'lucky db.verify_connection' to test postgres can be connected
+# * Checking that elasticsearch, redis, or postgres is installed and/or booted
+# * Note: Booting additional processes for things like mail, background jobs, etc...
+# should go in your Procfile.dev.
+
+
+if command_not_found "createdb"; then
+ MSG="Please install the postgres CLI tools, then try again."
+ if is_mac; then
+ MSG="$MSG\nIf you're using Postgres.app, see https://postgresapp.com/documentation/cli-tools.html."
+ fi
+ MSG="$MSG\nSee https://www.postgresql.org/docs/current/tutorial-install.html for install instructions."
+
+ print_error "$MSG"
+fi
+
+
+## CUSTOM PRE-BOOT CHECKS ##
+# example:
+# if command_not_running "redis-cli ping"; then
+# print_error "Redis is not running."
+# fi
+
+
diff --git a/fixtures/src_template/expected/spec/setup/clean_database.cr b/fixtures/src_template/expected/spec/setup/clean_database.cr
new file mode 100644
index 00000000..a1bc631c
--- /dev/null
+++ b/fixtures/src_template/expected/spec/setup/clean_database.cr
@@ -0,0 +1,3 @@
+Spec.before_each do
+ AppDatabase.truncate
+end
diff --git a/fixtures/src_template/expected/spec/setup/reset_emails.cr b/fixtures/src_template/expected/spec/setup/reset_emails.cr
new file mode 100644
index 00000000..140ab416
--- /dev/null
+++ b/fixtures/src_template/expected/spec/setup/reset_emails.cr
@@ -0,0 +1,3 @@
+Spec.before_each do
+ Carbon::DevAdapter.reset
+end
diff --git a/fixtures/src_template/expected/spec/setup/setup_database.cr b/fixtures/src_template/expected/spec/setup/setup_database.cr
new file mode 100644
index 00000000..393c6da3
--- /dev/null
+++ b/fixtures/src_template/expected/spec/setup/setup_database.cr
@@ -0,0 +1,2 @@
+Db::Create.new(quiet: true).call
+Db::Migrate.new(quiet: true).call
diff --git a/fixtures/src_template/expected/spec/setup/start_app_server.cr b/fixtures/src_template/expected/spec/setup/start_app_server.cr
new file mode 100644
index 00000000..3a64c702
--- /dev/null
+++ b/fixtures/src_template/expected/spec/setup/start_app_server.cr
@@ -0,0 +1,9 @@
+app_server = AppServer.new
+
+spawn do
+ app_server.listen
+end
+
+Spec.after_suite do
+ app_server.close
+end
diff --git a/fixtures/src_template/expected/spec/spec_helper.cr b/fixtures/src_template/expected/spec/spec_helper.cr
new file mode 100644
index 00000000..d876014d
--- /dev/null
+++ b/fixtures/src_template/expected/spec/spec_helper.cr
@@ -0,0 +1,19 @@
+ENV["LUCKY_ENV"] = "test"
+ENV["DEV_PORT"] = "5001"
+require "spec"
+require "../src/app"
+require "./support/**"
+require "../db/migrations/**"
+
+# Add/modify files in spec/setup to start/configure programs or run hooks
+#
+# By default there are scripts for setting up and cleaning the database,
+# configuring LuckyFlow, starting the app server, etc.
+require "./setup/**"
+
+include Carbon::Expectations
+include Lucky::RequestExpectations
+
+Avram::Migrator::Runner.new.ensure_migrated!
+Avram::SchemaEnforcer.ensure_correct_column_mappings!
+Habitat.raise_if_missing_settings!
diff --git a/fixtures/src_template/expected/spec/support/.keep b/fixtures/src_template/expected/spec/support/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template/expected/spec/support/api_client.cr b/fixtures/src_template/expected/spec/support/api_client.cr
new file mode 100644
index 00000000..46d449a8
--- /dev/null
+++ b/fixtures/src_template/expected/spec/support/api_client.cr
@@ -0,0 +1,12 @@
+class ApiClient < Lucky::BaseHTTPClient
+ app AppServer.new
+
+ def initialize
+ super
+ headers("Content-Type": "application/json")
+ end
+
+ def self.auth(user : User)
+ new.headers("Authorization": UserToken.generate(user))
+ end
+end
diff --git a/fixtures/src_template/expected/spec/support/factories/.keep b/fixtures/src_template/expected/spec/support/factories/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template/expected/src/actions/api_action.cr b/fixtures/src_template/expected/src/actions/api_action.cr
new file mode 100644
index 00000000..a16fd09e
--- /dev/null
+++ b/fixtures/src_template/expected/src/actions/api_action.cr
@@ -0,0 +1,17 @@
+# Include modules and add methods that are for all API requests
+abstract class ApiAction < Lucky::Action
+ # APIs typically do not need to send cookie/session data.
+ # Remove this line if you want to send cookies in the response header.
+ disable_cookies
+ accepted_formats [:json]
+
+ include Api::Auth::Helpers
+
+ # By default all actions require sign in.
+ # Add 'include Api::Auth::SkipRequireAuthToken' to your actions to allow all requests.
+ include Api::Auth::RequireAuthToken
+
+ # By default all actions are required to use underscores to separate words.
+ # Add 'include Lucky::SkipRouteStyleCheck' to your actions if you wish to ignore this check for specific routes.
+ include Lucky::EnforceUnderscoredRoute
+end
diff --git a/fixtures/src_template/expected/src/actions/errors/show.cr b/fixtures/src_template/expected/src/actions/errors/show.cr
new file mode 100644
index 00000000..a80eaa4d
--- /dev/null
+++ b/fixtures/src_template/expected/src/actions/errors/show.cr
@@ -0,0 +1,42 @@
+# This class handles error responses and reporting.
+#
+# https://luckyframework.org/guides/http-and-routing/error-handling
+class Errors::Show < Lucky::ErrorAction
+ DEFAULT_MESSAGE = "Something went wrong."
+ default_format :json
+ dont_report [Lucky::RouteNotFoundError, Avram::RecordNotFoundError]
+
+ def render(error : Lucky::RouteNotFoundError | Avram::RecordNotFoundError)
+ error_json "Not found", status: 404
+ end
+
+ # When an InvalidOperationError is raised, show a helpful error with the
+ # param that is invalid, and what was wrong with it.
+ def render(error : Avram::InvalidOperationError)
+ error_json \
+ message: error.renderable_message,
+ details: error.renderable_details,
+ param: error.invalid_attribute_name,
+ status: 400
+ end
+
+ # Always keep this below other 'render' methods or it may override your
+ # custom 'render' methods.
+ def render(error : Lucky::RenderableError)
+ error_json error.renderable_message, status: error.renderable_status
+ end
+
+ # If none of the 'render' methods return a response for the raised Exception,
+ # Lucky will use this method.
+ def default_render(error : Exception) : Lucky::Response
+ error_json DEFAULT_MESSAGE, status: 500
+ end
+
+ private def error_json(message : String, status : Int, details = nil, param = nil)
+ json ErrorSerializer.new(message: message, details: details, param: param), status: status
+ end
+
+ private def report(error : Exception) : Nil
+ # Send to Rollbar, send an email, etc.
+ end
+end
diff --git a/fixtures/src_template/expected/src/actions/home/index.cr b/fixtures/src_template/expected/src/actions/home/index.cr
new file mode 100644
index 00000000..5a72b779
--- /dev/null
+++ b/fixtures/src_template/expected/src/actions/home/index.cr
@@ -0,0 +1,7 @@
+class Home::Index < ApiAction
+ include Api::Auth::SkipRequireAuthToken
+
+ get "/" do
+ json({hello: "Hello World from Home::Index"})
+ end
+end
diff --git a/fixtures/src_template/expected/src/actions/mixins/.keep b/fixtures/src_template/expected/src/actions/mixins/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template/expected/src/app.cr b/fixtures/src_template/expected/src/app.cr
new file mode 100644
index 00000000..21fd1eac
--- /dev/null
+++ b/fixtures/src_template/expected/src/app.cr
@@ -0,0 +1,20 @@
+require "./shards"
+
+require "../config/server"
+require "./app_database"
+require "../config/**"
+require "./models/base_model"
+require "./models/mixins/**"
+require "./models/**"
+require "./queries/mixins/**"
+require "./queries/**"
+require "./operations/mixins/**"
+require "./operations/**"
+require "./serializers/base_serializer"
+require "./serializers/**"
+require "./emails/base_email"
+require "./emails/**"
+require "./actions/mixins/**"
+require "./actions/**"
+require "../db/migrations/**"
+require "./app_server"
diff --git a/fixtures/src_template/expected/src/app_database.cr b/fixtures/src_template/expected/src/app_database.cr
new file mode 100644
index 00000000..0efd4f50
--- /dev/null
+++ b/fixtures/src_template/expected/src/app_database.cr
@@ -0,0 +1,2 @@
+class AppDatabase < Avram::Database
+end
diff --git a/fixtures/src_template/expected/src/app_server.cr b/fixtures/src_template/expected/src/app_server.cr
new file mode 100644
index 00000000..53f483d1
--- /dev/null
+++ b/fixtures/src_template/expected/src/app_server.cr
@@ -0,0 +1,28 @@
+class AppServer < Lucky::BaseAppServer
+ # Learn about middleware with HTTP::Handlers:
+ # https://luckyframework.org/guides/http-and-routing/http-handlers
+ def middleware : Array(HTTP::Handler)
+ [
+ Lucky::RequestIdHandler.new,
+ Lucky::ForceSSLHandler.new,
+ Lucky::HttpMethodOverrideHandler.new,
+ Lucky::LogHandler.new,
+ Lucky::ErrorHandler.new(action: Errors::Show),
+ Lucky::RemoteIpHandler.new,
+ Lucky::RouteHandler.new,
+
+ # Disabled in API mode:
+ # Lucky::StaticCompressionHandler.new("./public", file_ext: "gz", content_encoding: "gzip"),
+ # Lucky::StaticFileHandler.new("./public", fallthrough: false, directory_listing: false),
+ Lucky::RouteNotFoundHandler.new,
+ ] of HTTP::Handler
+ end
+
+ def protocol
+ "http"
+ end
+
+ def listen
+ server.listen(host, port, reuse_port: false)
+ end
+end
diff --git a/fixtures/src_template/expected/src/emails/base_email.cr b/fixtures/src_template/expected/src/emails/base_email.cr
new file mode 100644
index 00000000..656f4f11
--- /dev/null
+++ b/fixtures/src_template/expected/src/emails/base_email.cr
@@ -0,0 +1,15 @@
+# Learn about sending emails
+# https://luckyframework.org/guides/emails/sending-emails-with-carbon
+abstract class BaseEmail < Carbon::Email
+ # You can add defaults using the 'inherited' hook
+ #
+ # Example:
+ #
+ # macro inherited
+ # from default_from
+ # end
+ #
+ # def default_from
+ # Carbon::Address.new("support@app.com")
+ # end
+end
diff --git a/fixtures/src_template/expected/src/models/base_model.cr b/fixtures/src_template/expected/src/models/base_model.cr
new file mode 100644
index 00000000..6bafeb84
--- /dev/null
+++ b/fixtures/src_template/expected/src/models/base_model.cr
@@ -0,0 +1,5 @@
+abstract class BaseModel < Avram::Model
+ def self.database : Avram::Database.class
+ AppDatabase
+ end
+end
diff --git a/fixtures/src_template/expected/src/models/mixins/.keep b/fixtures/src_template/expected/src/models/mixins/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template/expected/src/operations/.keep b/fixtures/src_template/expected/src/operations/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template/expected/src/operations/mixins/.keep b/fixtures/src_template/expected/src/operations/mixins/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template/expected/src/queries/.keep b/fixtures/src_template/expected/src/queries/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template/expected/src/queries/mixins/.keep b/fixtures/src_template/expected/src/queries/mixins/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template/expected/src/serializers/.keep b/fixtures/src_template/expected/src/serializers/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template/expected/src/serializers/base_serializer.cr b/fixtures/src_template/expected/src/serializers/base_serializer.cr
new file mode 100644
index 00000000..3ad0a669
--- /dev/null
+++ b/fixtures/src_template/expected/src/serializers/base_serializer.cr
@@ -0,0 +1,7 @@
+abstract class BaseSerializer < Lucky::Serializer
+ def self.for_collection(collection : Enumerable, *args, **named_args)
+ collection.map do |object|
+ new(object, *args, **named_args)
+ end
+ end
+end
diff --git a/fixtures/src_template/expected/src/serializers/error_serializer.cr b/fixtures/src_template/expected/src/serializers/error_serializer.cr
new file mode 100644
index 00000000..21a53aa2
--- /dev/null
+++ b/fixtures/src_template/expected/src/serializers/error_serializer.cr
@@ -0,0 +1,14 @@
+# This is the default error serializer generated by Lucky.
+# Feel free to customize it in any way you like.
+class ErrorSerializer < BaseSerializer
+ def initialize(
+ @message : String,
+ @details : String? = nil,
+ @param : String? = nil # so you can track which param (if any) caused the problem
+ )
+ end
+
+ def render
+ {message: @message, param: @param, details: @details}
+ end
+end
diff --git a/fixtures/src_template/expected/src/shards.cr b/fixtures/src_template/expected/src/shards.cr
new file mode 100644
index 00000000..7cadec18
--- /dev/null
+++ b/fixtures/src_template/expected/src/shards.cr
@@ -0,0 +1,10 @@
+# Load .env file before any other config or app code
+require "lucky_env"
+LuckyEnv.load?(".env")
+
+# Require your shards here
+require "lucky"
+require "avram/lucky"
+require "carbon"
+require "authentic"
+require "jwt"
diff --git a/fixtures/src_template/expected/src/start_server.cr b/fixtures/src_template/expected/src/start_server.cr
new file mode 100644
index 00000000..de8af78e
--- /dev/null
+++ b/fixtures/src_template/expected/src/start_server.cr
@@ -0,0 +1,17 @@
+require "./app"
+
+Habitat.raise_if_missing_settings!
+
+if LuckyEnv.development?
+ Avram::Migrator::Runner.new.ensure_migrated!
+ Avram::SchemaEnforcer.ensure_correct_column_mappings!
+end
+
+app_server = AppServer.new
+puts "Listening on http://#{app_server.host}:#{app_server.port}"
+
+Signal::INT.trap do
+ app_server.close
+end
+
+app_server.listen
diff --git a/fixtures/src_template/expected/src/test_project.cr b/fixtures/src_template/expected/src/test_project.cr
new file mode 100644
index 00000000..68e1a8d2
--- /dev/null
+++ b/fixtures/src_template/expected/src/test_project.cr
@@ -0,0 +1,6 @@
+# Typically you will not use or modify this file. 'shards build' and some
+# other crystal tools will sometimes use this.
+#
+# When this file is compiled/run it will require and run 'start_server',
+# which as its name implies will start the server for you app.
+require "./start_server"
diff --git a/fixtures/src_template/expected/tasks.cr b/fixtures/src_template/expected/tasks.cr
new file mode 100644
index 00000000..5a892d4d
--- /dev/null
+++ b/fixtures/src_template/expected/tasks.cr
@@ -0,0 +1,25 @@
+# This file loads your app and all your tasks when running 'lucky'
+#
+# Run 'lucky --help' to see all available tasks.
+#
+# Learn to create your own tasks:
+# https://luckyframework.org/guides/command-line-tasks/custom-tasks
+
+# See `LuckyEnv#task?`
+ENV["LUCKY_TASK"] = "true"
+
+# Load Lucky and the app (actions, models, etc.)
+require "./src/app"
+require "lucky_task"
+
+# You can add your own tasks here in the ./tasks folder
+require "./tasks/**"
+
+# Load migrations
+require "./db/migrations/**"
+
+# Load Lucky tasks (dev, routes, etc.)
+require "lucky/tasks/**"
+require "avram/lucky/tasks"
+
+LuckyTask::Runner.run
diff --git a/fixtures/src_template/expected/tasks/.keep b/fixtures/src_template/expected/tasks/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template/expected/tasks/db/seed/required_data.cr b/fixtures/src_template/expected/tasks/db/seed/required_data.cr
new file mode 100644
index 00000000..d866040f
--- /dev/null
+++ b/fixtures/src_template/expected/tasks/db/seed/required_data.cr
@@ -0,0 +1,30 @@
+require "../../../spec/support/factories/**"
+
+# Add seeds here that are *required* for your app to work.
+# For example, you might need at least one admin user or you might need at least
+# one category for your blog posts for the app to work.
+#
+# Use `Db::Seed::SampleData` if your only want to add sample data helpful for
+# development.
+class Db::Seed::RequiredData < LuckyTask::Task
+ summary "Add database records required for the app to work"
+
+ def call
+ # Using a Avram::Factory:
+ #
+ # Use the defaults, but override just the email
+ # UserFactory.create &.email("me@example.com")
+
+ # Using a SaveOperation:
+ #
+ # SaveUser.create!(email: "me@example.com", name: "Jane")
+ #
+ # You likely want to be able to run this file more than once. To do that,
+ # only create the record if it doesn't exist yet:
+ #
+ # unless UserQuery.new.email("me@example.com").first?
+ # SaveUser.create!(email: "me@example.com", name: "Jane")
+ # end
+ puts "Done adding required data"
+ end
+end
diff --git a/fixtures/src_template/expected/tasks/db/seed/sample_data.cr b/fixtures/src_template/expected/tasks/db/seed/sample_data.cr
new file mode 100644
index 00000000..231d7e8d
--- /dev/null
+++ b/fixtures/src_template/expected/tasks/db/seed/sample_data.cr
@@ -0,0 +1,30 @@
+require "../../../spec/support/factories/**"
+
+# Add sample data helpful for development, e.g. (fake users, blog posts, etc.)
+#
+# Use `Db::Seed::RequiredData` if you need to create data *required* for your
+# app to work.
+class Db::Seed::SampleData < LuckyTask::Task
+ summary "Add sample database records helpful for development"
+
+ def call
+ # Using an Avram::Factory:
+ #
+ # Use the defaults, but override just the email
+ # UserFactory.create &.email("me@example.com")
+
+ # Using a SaveOperation:
+ # ```
+ # SignUpUser.create!(email: "me@example.com", password: "test123", password_confirmation: "test123")
+ # ```
+ #
+ # You likely want to be able to run this file more than once. To do that,
+ # only create the record if it doesn't exist yet:
+ # ```
+ # if UserQuery.new.email("me@example.com").none?
+ # SignUpUser.create!(email: "me@example.com", password: "test123", password_confirmation: "test123")
+ # end
+ # ```
+ puts "Done adding sample data"
+ end
+end
diff --git a/fixtures/src_template__api_only/expected/.crystal-version b/fixtures/src_template__api_only/expected/.crystal-version
new file mode 100644
index 00000000..fdd3be6d
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/.crystal-version
@@ -0,0 +1 @@
+1.6.2
diff --git a/fixtures/src_template__api_only/expected/.env b/fixtures/src_template__api_only/expected/.env
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__api_only/expected/.github/workflows/ci.yml b/fixtures/src_template__api_only/expected/.github/workflows/ci.yml
new file mode 100644
index 00000000..2f4ae13b
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/.github/workflows/ci.yml
@@ -0,0 +1,90 @@
+name: test-project CI
+
+on:
+ push:
+ branches: "*"
+ pull_request:
+ branches: "*"
+
+jobs:
+ check-format:
+ strategy:
+ fail-fast: false
+ matrix:
+ crystal_version:
+ - 1.6.2
+ experimental:
+ - false
+ runs-on: ubuntu-latest
+ continue-on-error: ${{ matrix.experimental }}
+ steps:
+ - uses: actions/checkout@v2
+ - name: Install Crystal
+ uses: crystal-lang/install-crystal@v1
+ with:
+ crystal: ${{ matrix.crystal_version }}
+ - name: Format
+ run: crystal tool format --check
+
+ specs:
+ strategy:
+ fail-fast: false
+ matrix:
+ crystal_version:
+ - 1.6.2
+ experimental:
+ - false
+ runs-on: ubuntu-latest
+ env:
+ LUCKY_ENV: test
+ DB_HOST: localhost
+ continue-on-error: ${{ matrix.experimental }}
+ services:
+ postgres:
+ image: postgres:12-alpine
+ env:
+ POSTGRES_PASSWORD: postgres
+ ports:
+ - 5432:5432
+ # Set health checks to wait until postgres has started
+ options: >-
+ --health-cmd pg_isready
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Install Crystal
+ uses: crystal-lang/install-crystal@v1
+ with:
+ crystal: ${{ matrix.crystal_version }}
+
+ - name: Set up Crystal cache
+ uses: actions/cache@v2
+ id: crystal-cache
+ with:
+ path: |
+ ~/.cache/crystal
+ lib
+ lucky_tasks
+ key: ${{ runner.os }}-crystal-${{ hashFiles('**/shard.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-crystal-
+
+ - name: Install shards
+ if: steps.crystal-cache.outputs.cache-hit != 'true'
+ run: shards check || shards install
+
+ - name: Build lucky_tasks
+ if: steps.crystal-cache.outputs.cache-hit != 'true'
+ run: crystal build tasks.cr -o ./lucky_tasks
+
+ - name: Prepare database
+ run: |
+ ./lucky_tasks db.create
+ ./lucky_tasks db.migrate
+ ./lucky_tasks db.seed.required_data
+
+ - name: Run tests
+ run: crystal spec
\ No newline at end of file
diff --git a/fixtures/src_template__api_only/expected/Procfile b/fixtures/src_template__api_only/expected/Procfile
new file mode 100644
index 00000000..e524d70d
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/Procfile
@@ -0,0 +1,2 @@
+web: bin/app
+release: lucky db.migrate
diff --git a/fixtures/src_template__api_only/expected/Procfile.dev b/fixtures/src_template__api_only/expected/Procfile.dev
new file mode 100644
index 00000000..9cca7346
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/Procfile.dev
@@ -0,0 +1,2 @@
+system_check: script/system_check && sleep 100000
+web: lucky watch
diff --git a/fixtures/src_template__api_only/expected/README.md b/fixtures/src_template__api_only/expected/README.md
new file mode 100644
index 00000000..da2d97cc
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/README.md
@@ -0,0 +1,23 @@
+# test-project
+
+This is a project written using [Lucky](https://luckyframework.org). Enjoy!
+
+### Setting up the project
+
+1. [Install required dependencies](https://luckyframework.org/guides/getting-started/installing#install-required-dependencies)
+1. Update database settings in `config/database.cr`
+1. Run `script/setup`
+1. Run `lucky dev` to start the app
+
+### Using Docker for development
+
+1. [Install Docker](https://docs.docker.com/engine/install/)
+1. Run `docker compose up`
+
+The Docker container will boot all of the necessary components needed to run your Lucky application.
+To configure the container, update the `docker-compose.yml` file, and the `docker/development.dockerfile` file.
+
+
+### Learning Lucky
+
+Lucky uses the [Crystal](https://crystal-lang.org) programming language. You can learn about Lucky from the [Lucky Guides](https://luckyframework.org/guides/getting-started/why-lucky).
diff --git a/fixtures/src_template__api_only/expected/config/application.cr b/fixtures/src_template__api_only/expected/config/application.cr
new file mode 100644
index 00000000..c807149a
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/config/application.cr
@@ -0,0 +1,24 @@
+# This file may be used for custom Application configurations.
+# It will be loaded before other config files.
+#
+# Read more on configuration:
+# https://luckyframework.org/guides/getting-started/configuration#configuring-your-own-code
+
+# Use this code as an example:
+#
+# ```
+# module Application
+# Habitat.create do
+# setting support_email : String
+# setting lock_with_basic_auth : Bool
+# end
+# end
+#
+# Application.configure do |settings|
+# settings.support_email = "support@myapp.io"
+# settings.lock_with_basic_auth = LuckyEnv.staging?
+# end
+#
+# # In your application, call
+# # `Application.settings.support_email` anywhere you need it.
+# ```
diff --git a/fixtures/src_template__api_only/expected/config/colors.cr b/fixtures/src_template__api_only/expected/config/colors.cr
new file mode 100644
index 00000000..761ae940
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/config/colors.cr
@@ -0,0 +1,4 @@
+# This enables the color output when in development or test
+# Check out the Colorize docs for more information
+# https://crystal-lang.org/api/Colorize.html
+Colorize.enabled = LuckyEnv.development? || LuckyEnv.test?
diff --git a/fixtures/src_template__api_only/expected/config/cookies.cr b/fixtures/src_template__api_only/expected/config/cookies.cr
new file mode 100644
index 00000000..2d5055f2
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/config/cookies.cr
@@ -0,0 +1,25 @@
+require "./server"
+
+Lucky::Session.configure do |settings|
+ settings.key = "_test_project_session"
+end
+
+Lucky::CookieJar.configure do |settings|
+ settings.on_set = ->(cookie : HTTP::Cookie) {
+ # If ForceSSLHandler is enabled, only send cookies over HTTPS
+ cookie.secure(Lucky::ForceSSLHandler.settings.enabled)
+
+ # By default, don't allow reading cookies with JavaScript
+ cookie.http_only(true)
+
+ # Restrict cookies to a first-party or same-site context
+ cookie.samesite(:lax)
+
+ # Set all cookies to the root path by default
+ cookie.path("/")
+
+ # You can set other defaults for cookies here. For example:
+ #
+ # cookie.expires(1.year.from_now).domain("mydomain.com")
+ }
+end
diff --git a/fixtures/src_template__api_only/expected/config/database.cr b/fixtures/src_template__api_only/expected/config/database.cr
new file mode 100644
index 00000000..f614299a
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/config/database.cr
@@ -0,0 +1,29 @@
+database_name = "test_project_#{LuckyEnv.environment}"
+
+AppDatabase.configure do |settings|
+ if LuckyEnv.production?
+ settings.credentials = Avram::Credentials.parse(ENV["DATABASE_URL"])
+ else
+ settings.credentials = Avram::Credentials.parse?(ENV["DATABASE_URL"]?) || Avram::Credentials.new(
+ database: database_name,
+ hostname: ENV["DB_HOST"]? || "localhost",
+ port: ENV["DB_PORT"]?.try(&.to_i) || 5432,
+ # Some common usernames are "postgres", "root", or your system username (run 'whoami')
+ username: ENV["DB_USERNAME"]? || "postgres",
+ # Some Postgres installations require no password. Use "" if that is the case.
+ password: ENV["DB_PASSWORD"]? || "postgres"
+ )
+ end
+end
+
+Avram.configure do |settings|
+ settings.database_to_migrate = AppDatabase
+
+ # In production, allow lazy loading (N+1).
+ # In development and test, raise an error if you forget to preload associations
+ settings.lazy_load_enabled = LuckyEnv.production?
+
+ # Always parse `Time` values with these specific formats.
+ # Used for both database values, and datetime input fields.
+ # settings.time_formats << "%F"
+end
diff --git a/fixtures/src_template__api_only/expected/config/email.cr b/fixtures/src_template__api_only/expected/config/email.cr
new file mode 100644
index 00000000..7c875449
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/config/email.cr
@@ -0,0 +1,26 @@
+require "carbon_sendgrid_adapter"
+
+BaseEmail.configure do |settings|
+ if LuckyEnv.production?
+ # If you don't need to send emails, set the adapter to DevAdapter instead:
+ #
+ # settings.adapter = Carbon::DevAdapter.new
+ #
+ # If you do need emails, get a key from SendGrid and set an ENV variable
+ send_grid_key = send_grid_key_from_env
+ settings.adapter = Carbon::SendGridAdapter.new(api_key: send_grid_key)
+ elsif LuckyEnv.development?
+ settings.adapter = Carbon::DevAdapter.new(print_emails: true)
+ else
+ settings.adapter = Carbon::DevAdapter.new
+ end
+end
+
+private def send_grid_key_from_env
+ ENV["SEND_GRID_KEY"]? || raise_missing_key_message
+end
+
+private def raise_missing_key_message
+ puts "Missing SEND_GRID_KEY. Set the SEND_GRID_KEY env variable to 'unused' if not sending emails, or set the SEND_GRID_KEY ENV var.".colorize.red
+ exit(1)
+end
diff --git a/fixtures/src_template__api_only/expected/config/env.cr b/fixtures/src_template__api_only/expected/config/env.cr
new file mode 100644
index 00000000..3f364072
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/config/env.cr
@@ -0,0 +1,33 @@
+# Environments are managed using `LuckyEnv`. By default, development, production
+# and test are supported. See
+# https://luckyframework.org/guides/getting-started/configuration for details.
+#
+# The default environment is development unless the environment variable
+# LUCKY_ENV is set.
+#
+# Example:
+# ```
+# LuckyEnv.environment # => "development"
+# LuckyEnv.development? # => true
+# LuckyEnv.production? # => false
+# LuckyEnv.test? # => false
+# ```
+#
+# New environments can be added using the `LuckyEnv.add_env` macro.
+#
+# Example:
+# ```
+# LuckyEnv.add_env :staging
+# LuckyEnv.staging? # => false
+# ```
+#
+# To determine whether or not a `LuckyTask` is currently running, you can use
+# the `LuckyEnv.task?` predicate.
+#
+# Example:
+# ```
+# LuckyEnv.task? # => false
+# ```
+
+# Add a staging environment.
+# LuckyEnv.add_env :staging
diff --git a/fixtures/src_template__api_only/expected/config/error_handler.cr b/fixtures/src_template__api_only/expected/config/error_handler.cr
new file mode 100644
index 00000000..c6b736e3
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/config/error_handler.cr
@@ -0,0 +1,3 @@
+Lucky::ErrorHandler.configure do |settings|
+ settings.show_debug_output = !LuckyEnv.production?
+end
diff --git a/fixtures/src_template__api_only/expected/config/log.cr b/fixtures/src_template__api_only/expected/config/log.cr
new file mode 100644
index 00000000..0e8d7636
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/config/log.cr
@@ -0,0 +1,45 @@
+require "file_utils"
+
+if LuckyEnv.test?
+ # Logs to `tmp/test.log` so you can see what's happening without having
+ # a bunch of log output in your spec results.
+ FileUtils.mkdir_p("tmp")
+
+ backend = Log::IOBackend.new(File.new("tmp/test.log", mode: "w"))
+ backend.formatter = Lucky::PrettyLogFormatter.proc
+ Log.dexter.configure(:debug, backend)
+elsif LuckyEnv.production?
+ # Lucky uses JSON in production so logs can be searched more easily
+ #
+ # If you want logs like in develpoment use 'Lucky::PrettyLogFormatter.proc'.
+ backend = Log::IOBackend.new
+ backend.formatter = Dexter::JSONLogFormatter.proc
+ Log.dexter.configure(:info, backend)
+else
+ # Use a pretty formatter printing to STDOUT in development
+ backend = Log::IOBackend.new
+ backend.formatter = Lucky::PrettyLogFormatter.proc
+ Log.dexter.configure(:debug, backend)
+ DB::Log.level = :info
+end
+
+# Lucky only logs when before/after pipes halt by redirecting, or rendering a
+# response. Pipes that run without halting are not logged.
+#
+# If you want to log every pipe that runs, set the log level to ':info'
+Lucky::ContinuedPipeLog.dexter.configure(:none)
+
+# Lucky only logs failed queries by default.
+#
+# Set the log to ':info' to log all queries
+Avram::QueryLog.dexter.configure(:none)
+
+# Skip logging static assets requests in development
+Lucky::LogHandler.configure do |settings|
+ if LuckyEnv.development?
+ settings.skip_if = ->(context : HTTP::Server::Context) {
+ context.request.method.downcase == "get" &&
+ context.request.resource.starts_with?(/\/css\/|\/js\/|\/assets\/|\/favicon\.ico/)
+ }
+ end
+end
diff --git a/fixtures/src_template__api_only/expected/config/route_helper.cr b/fixtures/src_template__api_only/expected/config/route_helper.cr
new file mode 100644
index 00000000..ede1f328
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/config/route_helper.cr
@@ -0,0 +1,10 @@
+# This is used when generating URLs for your application
+Lucky::RouteHelper.configure do |settings|
+ if LuckyEnv.production?
+ # Example: https://my_app.com
+ settings.base_uri = ENV.fetch("APP_DOMAIN")
+ else
+ # Set domain to the default host/port in development/test
+ settings.base_uri = "http://localhost:#{Lucky::ServerSettings.port}"
+ end
+end
diff --git a/fixtures/src_template__api_only/expected/config/server.cr b/fixtures/src_template__api_only/expected/config/server.cr
new file mode 100644
index 00000000..c72314be
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/config/server.cr
@@ -0,0 +1,65 @@
+# Here is where you configure the Lucky server
+#
+# Look at config/route_helper.cr if you want to change the domain used when
+# generating links with `Action.url`.
+Lucky::Server.configure do |settings|
+ if LuckyEnv.production?
+ settings.secret_key_base = secret_key_from_env
+ settings.host = "0.0.0.0"
+ settings.port = ENV["PORT"].to_i
+ settings.gzip_enabled = true
+ # By default certain content types will be gzipped.
+ # For a full list look in
+ # https://github.com/luckyframework/lucky/blob/main/src/lucky/server.cr
+ # To add additional extensions do something like this:
+ # settings.gzip_content_types << "content/type"
+ else
+ settings.secret_key_base = "1234567890"
+ # Change host/port in config/watch.yml
+ # Alternatively, you can set the DEV_PORT env to set the port for local development
+ settings.host = Lucky::ServerSettings.host
+ settings.port = Lucky::ServerSettings.port
+ end
+
+ # By default Lucky will serve static assets in development and production.
+ #
+ # However you could use a CDN when in production like this:
+ #
+ # Lucky::Server.configure do |settings|
+ # if LuckyEnv.production?
+ # settings.asset_host = "https://mycdnhost.com"
+ # else
+ # settings.asset_host = ""
+ # end
+ # end
+ settings.asset_host = "" # Lucky will serve assets
+end
+
+Lucky::ForceSSLHandler.configure do |settings|
+ # To force SSL in production, uncomment the lines below.
+ # This will cause http requests to be redirected to https:
+ #
+ # settings.enabled = LuckyEnv.production?
+ # settings.strict_transport_security = {max_age: 1.year, include_subdomains: true}
+ #
+ # Or, leave it disabled:
+ settings.enabled = false
+end
+
+# Set a unique ID for each HTTP request.
+# To enable the request ID, uncomment the lines below.
+# You can set your own custom String, or use a random UUID.
+# Lucky::RequestIdHandler.configure do |settings|
+# settings.set_request_id = ->(context : HTTP::Server::Context) {
+# UUID.random.to_s
+# }
+# end
+
+private def secret_key_from_env
+ ENV["SECRET_KEY_BASE"]? || raise_missing_secret_key_in_production
+end
+
+private def raise_missing_secret_key_in_production
+ puts "Please set the SECRET_KEY_BASE environment variable. You can generate a secret key with 'lucky gen.secret_key'".colorize.red
+ exit(1)
+end
diff --git a/fixtures/src_template__api_only/expected/config/watch.yml b/fixtures/src_template__api_only/expected/config/watch.yml
new file mode 100644
index 00000000..3a59b410
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/config/watch.yml
@@ -0,0 +1,3 @@
+host: 127.0.0.1
+port: 3000
+reload_port: 3001
diff --git a/fixtures/src_template__api_only/expected/db/migrations/.keep b/fixtures/src_template__api_only/expected/db/migrations/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__api_only/expected/docker-compose.yml b/fixtures/src_template__api_only/expected/docker-compose.yml
new file mode 100644
index 00000000..d00779db
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/docker-compose.yml
@@ -0,0 +1,45 @@
+version: "3.8"
+services:
+ lucky:
+ build:
+ context: .
+ dockerfile: docker/development.dockerfile
+ environment:
+ DATABASE_URL: postgres://lucky:password@postgres:5432/lucky
+ DEV_HOST: "0.0.0.0"
+ volumes:
+ - .:/app
+ - node_modules:/app/node_modules
+ - shards_lib:/app/lib
+ - app_bin:/app/bin
+ - build_cache:/root/.cache
+ depends_on:
+ - postgres
+ ports:
+ - 3000:3000 # This is the Lucky Server port
+ - 3001:3001 # This is the Lucky watcher reload port
+
+ entrypoint: ["docker/dev_entrypoint.sh"]
+
+ postgres:
+ image: postgres:14-alpine
+ environment:
+ POSTGRES_USER: lucky
+ POSTGRES_PASSWORD: password
+ POSTGRES_DB: lucky
+ volumes:
+ - postgres_data:/var/lib/postgresql/data
+ ports:
+ # The postgres database container is exposed on the host at port 6543 to
+ # allow connecting directly to it with postgres clients. The port differs
+ # from the postgres default to avoid conflict with existing postgres
+ # servers. Connect to a running postgres container with:
+ # postgres://lucky:password@localhost:6543/lucky
+ - 6543:5432
+
+volumes:
+ postgres_data:
+ node_modules:
+ shards_lib:
+ app_bin:
+ build_cache:
diff --git a/fixtures/src_template__api_only/expected/docker/dev_entrypoint.sh b/fixtures/src_template__api_only/expected/docker/dev_entrypoint.sh
new file mode 100755
index 00000000..f5aab877
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/docker/dev_entrypoint.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+
+set -euo pipefail
+
+# This is the entrypoint script used for development docker workflows.
+# By default it will:
+# - Install dependencies.
+# - Run migrations.
+# - Start the dev server.
+# It also accepts any commands to be run instead.
+
+
+warnfail () {
+ echo "$@" >&2
+ exit 1
+}
+
+case ${1:-} in
+ "") # If no arguments are provided, start lucky dev server.
+ ;;
+
+ *) # If any arguments are provided, execute them instead.
+ exec "$@"
+esac
+
+if ! [ -d bin ] ; then
+ echo 'Creating bin directory'
+ mkdir bin
+fi
+if ! shards check ; then
+ echo 'Installing shards...'
+ shards install
+fi
+
+echo 'Waiting for postgres to be available...'
+./docker/wait-for-it.sh -q postgres:5432
+
+if ! psql -d "$DATABASE_URL" -c '\d migrations' > /dev/null ; then
+ echo 'Finishing database setup...'
+ lucky db.migrate
+fi
+
+echo 'Starting lucky dev server...'
+exec lucky dev
diff --git a/fixtures/src_template__api_only/expected/docker/development.dockerfile b/fixtures/src_template__api_only/expected/docker/development.dockerfile
new file mode 100644
index 00000000..11222751
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/docker/development.dockerfile
@@ -0,0 +1,25 @@
+FROM crystallang/crystal:1.6.2
+
+# Install utilities required to make this Dockerfile run
+RUN apt-get update && \
+ apt-get install -y wget
+
+# Apt installs:
+# - Postgres cli tools are required for lucky-cli.
+# - tmux is required for the Overmind process manager.
+RUN apt-get update && \
+ apt-get install -y postgresql-client tmux && \
+ rm -rf /var/lib/apt/lists/*
+
+# Install lucky cli
+WORKDIR /lucky/cli
+RUN git clone https://github.com/luckyframework/lucky_cli . && \
+ git checkout v1.0.0 && \
+ shards build --without-development && \
+ cp bin/lucky /usr/bin
+
+WORKDIR /app
+ENV DATABASE_URL=postgres://postgres:postgres@host.docker.internal:5432/postgres
+EXPOSE 3000
+EXPOSE 3001
+
diff --git a/fixtures/src_template__api_only/expected/docker/wait-for-it.sh b/fixtures/src_template__api_only/expected/docker/wait-for-it.sh
new file mode 100755
index 00000000..06e0638c
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/docker/wait-for-it.sh
@@ -0,0 +1,189 @@
+#!/usr/bin/bash
+#
+# Pulled from https://github.com/vishnubob/wait-for-it on 2022-02-28.
+# Licensed under the MIT license as of 81b1373f.
+#
+# Below this line, wait-for-it is the original work of the author.
+#
+# Use this script to test if a given TCP host/port are available
+
+WAITFORIT_cmdname=${0##*/}
+
+echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }
+
+usage()
+{
+ cat << USAGE >&2
+Usage:
+ $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args]
+ -h HOST | --host=HOST Host or IP under test
+ -p PORT | --port=PORT TCP port under test
+ Alternatively, you specify the host and port as host:port
+ -s | --strict Only execute subcommand if the test succeeds
+ -q | --quiet Don't output any status messages
+ -t TIMEOUT | --timeout=TIMEOUT
+ Timeout in seconds, zero for no timeout
+ -- COMMAND ARGS Execute command with args after the test finishes
+USAGE
+ exit 1
+}
+
+wait_for()
+{
+ if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
+ echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
+ else
+ echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout"
+ fi
+ WAITFORIT_start_ts=$(date +%s)
+ while :
+ do
+ if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then
+ nc -z $WAITFORIT_HOST $WAITFORIT_PORT
+ WAITFORIT_result=$?
+ else
+ (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1
+ WAITFORIT_result=$?
+ fi
+ if [[ $WAITFORIT_result -eq 0 ]]; then
+ WAITFORIT_end_ts=$(date +%s)
+ echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds"
+ break
+ fi
+ sleep 1
+ done
+ return $WAITFORIT_result
+}
+
+wait_for_wrapper()
+{
+ # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
+ if [[ $WAITFORIT_QUIET -eq 1 ]]; then
+ timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
+ else
+ timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
+ fi
+ WAITFORIT_PID=$!
+ trap "kill -INT -$WAITFORIT_PID" INT
+ wait $WAITFORIT_PID
+ WAITFORIT_RESULT=$?
+ if [[ $WAITFORIT_RESULT -ne 0 ]]; then
+ echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
+ fi
+ return $WAITFORIT_RESULT
+}
+
+# process arguments
+while [[ $# -gt 0 ]]
+do
+ case "$1" in
+ *:* )
+ WAITFORIT_hostport=(${1//:/ })
+ WAITFORIT_HOST=${WAITFORIT_hostport[0]}
+ WAITFORIT_PORT=${WAITFORIT_hostport[1]}
+ shift 1
+ ;;
+ --child)
+ WAITFORIT_CHILD=1
+ shift 1
+ ;;
+ -q | --quiet)
+ WAITFORIT_QUIET=1
+ shift 1
+ ;;
+ -s | --strict)
+ WAITFORIT_STRICT=1
+ shift 1
+ ;;
+ -h)
+ WAITFORIT_HOST="$2"
+ if [[ $WAITFORIT_HOST == "" ]]; then break; fi
+ shift 2
+ ;;
+ --host=*)
+ WAITFORIT_HOST="${1#*=}"
+ shift 1
+ ;;
+ -p)
+ WAITFORIT_PORT="$2"
+ if [[ $WAITFORIT_PORT == "" ]]; then break; fi
+ shift 2
+ ;;
+ --port=*)
+ WAITFORIT_PORT="${1#*=}"
+ shift 1
+ ;;
+ -t)
+ WAITFORIT_TIMEOUT="$2"
+ if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi
+ shift 2
+ ;;
+ --timeout=*)
+ WAITFORIT_TIMEOUT="${1#*=}"
+ shift 1
+ ;;
+ --)
+ shift
+ WAITFORIT_CLI=("$@")
+ break
+ ;;
+ --help)
+ usage
+ ;;
+ *)
+ echoerr "Unknown argument: $1"
+ usage
+ ;;
+ esac
+done
+
+if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then
+ echoerr "Error: you need to provide a host and port to test."
+ usage
+fi
+
+WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15}
+WAITFORIT_STRICT=${WAITFORIT_STRICT:-0}
+WAITFORIT_CHILD=${WAITFORIT_CHILD:-0}
+WAITFORIT_QUIET=${WAITFORIT_QUIET:-0}
+
+# Check to see if timeout is from busybox?
+WAITFORIT_TIMEOUT_PATH=$(type -p timeout)
+WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH)
+
+WAITFORIT_BUSYTIMEFLAG=""
+if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then
+ WAITFORIT_ISBUSY=1
+ # Check if busybox timeout uses -t flag
+ # (recent Alpine versions don't support -t anymore)
+ if timeout &>/dev/stdout | grep -q -e '-t '; then
+ WAITFORIT_BUSYTIMEFLAG="-t"
+ fi
+else
+ WAITFORIT_ISBUSY=0
+fi
+
+if [[ $WAITFORIT_CHILD -gt 0 ]]; then
+ wait_for
+ WAITFORIT_RESULT=$?
+ exit $WAITFORIT_RESULT
+else
+ if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
+ wait_for_wrapper
+ WAITFORIT_RESULT=$?
+ else
+ wait_for
+ WAITFORIT_RESULT=$?
+ fi
+fi
+
+if [[ $WAITFORIT_CLI != "" ]]; then
+ if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then
+ echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess"
+ exit $WAITFORIT_RESULT
+ fi
+ exec "${WAITFORIT_CLI[@]}"
+else
+ exit $WAITFORIT_RESULT
+fi
+
diff --git a/fixtures/src_template__api_only/expected/script/helpers/function_helpers b/fixtures/src_template__api_only/expected/script/helpers/function_helpers
new file mode 100644
index 00000000..388fa67e
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/script/helpers/function_helpers
@@ -0,0 +1,67 @@
+#!/usr/bin/env bash
+
+# This file contains a set of functions used as helpers
+# for various tasks. Read the examples for each one for
+# more information. Feel free to put any additional helper
+# functions you may need for your app
+
+
+# Returns true if the command $1 is not found
+# example:
+# if command_not_found "yarn"; then
+# echo "no yarn"
+# fi
+command_not_found() {
+ ! command -v $1 > /dev/null
+ return $?
+}
+
+# Returns true if the command $1 is not running
+# You must supply the full command to check as an argument
+# example:
+# if command_not_running "redis-cli ping"; then
+# print_error "Redis is not running"
+# fi
+command_not_running() {
+ $1
+ if [ $? -ne 0 ]; then
+ true
+ else
+ false
+ fi
+}
+
+# Returns true if the OS is macOS
+# example:
+# if is_mac; then
+# echo "do mac stuff"
+# fi
+is_mac() {
+ if [[ "$OSTYPE" == "darwin"* ]]; then
+ true
+ else
+ false
+ fi
+}
+
+# Returns true if the OS is linux based
+# example:
+# if is_linux; then
+# echo "do linux stuff"
+# fi
+is_linux() {
+ if [[ "$OSTYPE" == "linux"* ]]; then
+ true
+ else
+ false
+ fi
+}
+
+# Prints error and exit.
+# example:
+# print_error "Redis is not running. Run it with some_command"
+print_error() {
+ printf "${BOLD_RED_COLOR}There is a problem with your system setup:\n\n"
+ printf "${BOLD_RED_COLOR}$1 \n\n" | indent
+ exit 1
+}
diff --git a/fixtures/src_template__api_only/expected/script/helpers/text_helpers b/fixtures/src_template__api_only/expected/script/helpers/text_helpers
new file mode 100644
index 00000000..34b77a8c
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/script/helpers/text_helpers
@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+
+# This file contains a set of functions used to format text,
+# and make printing text a little easier. Feel free to put
+# any additional functions you need for formatting your shell
+# output text.
+
+# Colors
+BOLD_RED_COLOR="\e[1m\e[31m"
+
+# Indents the text 2 spaces
+# example:
+# printf "Hello" | indent
+indent() {
+ while read LINE; do
+ echo " $LINE" || true
+ done
+}
+
+# Prints out an arrow to your custom notice
+# example:
+# notice "Installing new magic"
+notice() {
+ printf "\n▸ $1\n"
+}
+
+# Prints out a check mark and Done.
+# example:
+# print_done
+print_done() {
+ printf "✔ Done\n" | indent
+}
diff --git a/fixtures/src_template__api_only/expected/script/setup b/fixtures/src_template__api_only/expected/script/setup
new file mode 100755
index 00000000..5a97531e
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/script/setup
@@ -0,0 +1,38 @@
+#!/usr/bin/env bash
+
+# Exit if any subcommand fails
+set -e
+set -o pipefail
+
+source script/helpers/text_helpers
+
+
+notice "Running System Check"
+./script/system_check
+print_done
+
+
+notice "Installing shards"
+shards install --ignore-crystal-version | indent
+
+if [ ! -f ".env" ]; then
+ notice "No .env found. Creating one."
+ touch .env
+ print_done
+fi
+
+notice "Creating the database"
+lucky db.create | indent
+
+notice "Verifying postgres connection"
+lucky db.verify_connection | indent
+
+notice "Migrating the database"
+lucky db.migrate | indent
+
+notice "Seeding the database with required and sample records"
+lucky db.seed.required_data | indent
+lucky db.seed.sample_data | indent
+
+print_done
+notice "Run 'lucky dev' to start the app"
diff --git a/fixtures/src_template__api_only/expected/script/system_check b/fixtures/src_template__api_only/expected/script/system_check
new file mode 100755
index 00000000..eea2533a
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/script/system_check
@@ -0,0 +1,34 @@
+#!/usr/bin/env bash
+
+source script/helpers/text_helpers
+source script/helpers/function_helpers
+
+# Use this script to check the system for required tools and process that your app needs.
+# A few helper functions are provided to make writing bash a little easier. See the
+# script/helpers/function_helpers file for more examples.
+#
+# A few examples you might use here:
+# * 'lucky db.verify_connection' to test postgres can be connected
+# * Checking that elasticsearch, redis, or postgres is installed and/or booted
+# * Note: Booting additional processes for things like mail, background jobs, etc...
+# should go in your Procfile.dev.
+
+
+if command_not_found "createdb"; then
+ MSG="Please install the postgres CLI tools, then try again."
+ if is_mac; then
+ MSG="$MSG\nIf you're using Postgres.app, see https://postgresapp.com/documentation/cli-tools.html."
+ fi
+ MSG="$MSG\nSee https://www.postgresql.org/docs/current/tutorial-install.html for install instructions."
+
+ print_error "$MSG"
+fi
+
+
+## CUSTOM PRE-BOOT CHECKS ##
+# example:
+# if command_not_running "redis-cli ping"; then
+# print_error "Redis is not running."
+# fi
+
+
diff --git a/fixtures/src_template__api_only/expected/spec/setup/clean_database.cr b/fixtures/src_template__api_only/expected/spec/setup/clean_database.cr
new file mode 100644
index 00000000..a1bc631c
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/spec/setup/clean_database.cr
@@ -0,0 +1,3 @@
+Spec.before_each do
+ AppDatabase.truncate
+end
diff --git a/fixtures/src_template__api_only/expected/spec/setup/reset_emails.cr b/fixtures/src_template__api_only/expected/spec/setup/reset_emails.cr
new file mode 100644
index 00000000..140ab416
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/spec/setup/reset_emails.cr
@@ -0,0 +1,3 @@
+Spec.before_each do
+ Carbon::DevAdapter.reset
+end
diff --git a/fixtures/src_template__api_only/expected/spec/setup/setup_database.cr b/fixtures/src_template__api_only/expected/spec/setup/setup_database.cr
new file mode 100644
index 00000000..393c6da3
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/spec/setup/setup_database.cr
@@ -0,0 +1,2 @@
+Db::Create.new(quiet: true).call
+Db::Migrate.new(quiet: true).call
diff --git a/fixtures/src_template__api_only/expected/spec/setup/start_app_server.cr b/fixtures/src_template__api_only/expected/spec/setup/start_app_server.cr
new file mode 100644
index 00000000..3a64c702
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/spec/setup/start_app_server.cr
@@ -0,0 +1,9 @@
+app_server = AppServer.new
+
+spawn do
+ app_server.listen
+end
+
+Spec.after_suite do
+ app_server.close
+end
diff --git a/fixtures/src_template__api_only/expected/spec/spec_helper.cr b/fixtures/src_template__api_only/expected/spec/spec_helper.cr
new file mode 100644
index 00000000..d876014d
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/spec/spec_helper.cr
@@ -0,0 +1,19 @@
+ENV["LUCKY_ENV"] = "test"
+ENV["DEV_PORT"] = "5001"
+require "spec"
+require "../src/app"
+require "./support/**"
+require "../db/migrations/**"
+
+# Add/modify files in spec/setup to start/configure programs or run hooks
+#
+# By default there are scripts for setting up and cleaning the database,
+# configuring LuckyFlow, starting the app server, etc.
+require "./setup/**"
+
+include Carbon::Expectations
+include Lucky::RequestExpectations
+
+Avram::Migrator::Runner.new.ensure_migrated!
+Avram::SchemaEnforcer.ensure_correct_column_mappings!
+Habitat.raise_if_missing_settings!
diff --git a/fixtures/src_template__api_only/expected/spec/support/.keep b/fixtures/src_template__api_only/expected/spec/support/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__api_only/expected/spec/support/api_client.cr b/fixtures/src_template__api_only/expected/spec/support/api_client.cr
new file mode 100644
index 00000000..ef251251
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/spec/support/api_client.cr
@@ -0,0 +1,8 @@
+class ApiClient < Lucky::BaseHTTPClient
+ app AppServer.new
+
+ def initialize
+ super
+ headers("Content-Type": "application/json")
+ end
+end
diff --git a/fixtures/src_template__api_only/expected/spec/support/factories/.keep b/fixtures/src_template__api_only/expected/spec/support/factories/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__api_only/expected/src/actions/api_action.cr b/fixtures/src_template__api_only/expected/src/actions/api_action.cr
new file mode 100644
index 00000000..fac02c8b
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/src/actions/api_action.cr
@@ -0,0 +1,11 @@
+# Include modules and add methods that are for all API requests
+abstract class ApiAction < Lucky::Action
+ # APIs typically do not need to send cookie/session data.
+ # Remove this line if you want to send cookies in the response header.
+ disable_cookies
+ accepted_formats [:json]
+
+ # By default all actions are required to use underscores to separate words.
+ # Add 'include Lucky::SkipRouteStyleCheck' to your actions if you wish to ignore this check for specific routes.
+ include Lucky::EnforceUnderscoredRoute
+end
diff --git a/fixtures/src_template__api_only/expected/src/actions/errors/show.cr b/fixtures/src_template__api_only/expected/src/actions/errors/show.cr
new file mode 100644
index 00000000..a80eaa4d
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/src/actions/errors/show.cr
@@ -0,0 +1,42 @@
+# This class handles error responses and reporting.
+#
+# https://luckyframework.org/guides/http-and-routing/error-handling
+class Errors::Show < Lucky::ErrorAction
+ DEFAULT_MESSAGE = "Something went wrong."
+ default_format :json
+ dont_report [Lucky::RouteNotFoundError, Avram::RecordNotFoundError]
+
+ def render(error : Lucky::RouteNotFoundError | Avram::RecordNotFoundError)
+ error_json "Not found", status: 404
+ end
+
+ # When an InvalidOperationError is raised, show a helpful error with the
+ # param that is invalid, and what was wrong with it.
+ def render(error : Avram::InvalidOperationError)
+ error_json \
+ message: error.renderable_message,
+ details: error.renderable_details,
+ param: error.invalid_attribute_name,
+ status: 400
+ end
+
+ # Always keep this below other 'render' methods or it may override your
+ # custom 'render' methods.
+ def render(error : Lucky::RenderableError)
+ error_json error.renderable_message, status: error.renderable_status
+ end
+
+ # If none of the 'render' methods return a response for the raised Exception,
+ # Lucky will use this method.
+ def default_render(error : Exception) : Lucky::Response
+ error_json DEFAULT_MESSAGE, status: 500
+ end
+
+ private def error_json(message : String, status : Int, details = nil, param = nil)
+ json ErrorSerializer.new(message: message, details: details, param: param), status: status
+ end
+
+ private def report(error : Exception) : Nil
+ # Send to Rollbar, send an email, etc.
+ end
+end
diff --git a/fixtures/src_template__api_only/expected/src/actions/home/index.cr b/fixtures/src_template__api_only/expected/src/actions/home/index.cr
new file mode 100644
index 00000000..eb326da9
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/src/actions/home/index.cr
@@ -0,0 +1,5 @@
+class Home::Index < ApiAction
+ get "/" do
+ json({hello: "Hello World from Home::Index"})
+ end
+end
diff --git a/fixtures/src_template__api_only/expected/src/actions/mixins/.keep b/fixtures/src_template__api_only/expected/src/actions/mixins/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__api_only/expected/src/app.cr b/fixtures/src_template__api_only/expected/src/app.cr
new file mode 100644
index 00000000..21fd1eac
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/src/app.cr
@@ -0,0 +1,20 @@
+require "./shards"
+
+require "../config/server"
+require "./app_database"
+require "../config/**"
+require "./models/base_model"
+require "./models/mixins/**"
+require "./models/**"
+require "./queries/mixins/**"
+require "./queries/**"
+require "./operations/mixins/**"
+require "./operations/**"
+require "./serializers/base_serializer"
+require "./serializers/**"
+require "./emails/base_email"
+require "./emails/**"
+require "./actions/mixins/**"
+require "./actions/**"
+require "../db/migrations/**"
+require "./app_server"
diff --git a/fixtures/src_template__api_only/expected/src/app_database.cr b/fixtures/src_template__api_only/expected/src/app_database.cr
new file mode 100644
index 00000000..0efd4f50
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/src/app_database.cr
@@ -0,0 +1,2 @@
+class AppDatabase < Avram::Database
+end
diff --git a/fixtures/src_template__api_only/expected/src/app_server.cr b/fixtures/src_template__api_only/expected/src/app_server.cr
new file mode 100644
index 00000000..53f483d1
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/src/app_server.cr
@@ -0,0 +1,28 @@
+class AppServer < Lucky::BaseAppServer
+ # Learn about middleware with HTTP::Handlers:
+ # https://luckyframework.org/guides/http-and-routing/http-handlers
+ def middleware : Array(HTTP::Handler)
+ [
+ Lucky::RequestIdHandler.new,
+ Lucky::ForceSSLHandler.new,
+ Lucky::HttpMethodOverrideHandler.new,
+ Lucky::LogHandler.new,
+ Lucky::ErrorHandler.new(action: Errors::Show),
+ Lucky::RemoteIpHandler.new,
+ Lucky::RouteHandler.new,
+
+ # Disabled in API mode:
+ # Lucky::StaticCompressionHandler.new("./public", file_ext: "gz", content_encoding: "gzip"),
+ # Lucky::StaticFileHandler.new("./public", fallthrough: false, directory_listing: false),
+ Lucky::RouteNotFoundHandler.new,
+ ] of HTTP::Handler
+ end
+
+ def protocol
+ "http"
+ end
+
+ def listen
+ server.listen(host, port, reuse_port: false)
+ end
+end
diff --git a/fixtures/src_template__api_only/expected/src/emails/base_email.cr b/fixtures/src_template__api_only/expected/src/emails/base_email.cr
new file mode 100644
index 00000000..656f4f11
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/src/emails/base_email.cr
@@ -0,0 +1,15 @@
+# Learn about sending emails
+# https://luckyframework.org/guides/emails/sending-emails-with-carbon
+abstract class BaseEmail < Carbon::Email
+ # You can add defaults using the 'inherited' hook
+ #
+ # Example:
+ #
+ # macro inherited
+ # from default_from
+ # end
+ #
+ # def default_from
+ # Carbon::Address.new("support@app.com")
+ # end
+end
diff --git a/fixtures/src_template__api_only/expected/src/models/base_model.cr b/fixtures/src_template__api_only/expected/src/models/base_model.cr
new file mode 100644
index 00000000..6bafeb84
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/src/models/base_model.cr
@@ -0,0 +1,5 @@
+abstract class BaseModel < Avram::Model
+ def self.database : Avram::Database.class
+ AppDatabase
+ end
+end
diff --git a/fixtures/src_template__api_only/expected/src/models/mixins/.keep b/fixtures/src_template__api_only/expected/src/models/mixins/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__api_only/expected/src/operations/.keep b/fixtures/src_template__api_only/expected/src/operations/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__api_only/expected/src/operations/mixins/.keep b/fixtures/src_template__api_only/expected/src/operations/mixins/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__api_only/expected/src/queries/.keep b/fixtures/src_template__api_only/expected/src/queries/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__api_only/expected/src/queries/mixins/.keep b/fixtures/src_template__api_only/expected/src/queries/mixins/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__api_only/expected/src/serializers/.keep b/fixtures/src_template__api_only/expected/src/serializers/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__api_only/expected/src/serializers/base_serializer.cr b/fixtures/src_template__api_only/expected/src/serializers/base_serializer.cr
new file mode 100644
index 00000000..3ad0a669
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/src/serializers/base_serializer.cr
@@ -0,0 +1,7 @@
+abstract class BaseSerializer < Lucky::Serializer
+ def self.for_collection(collection : Enumerable, *args, **named_args)
+ collection.map do |object|
+ new(object, *args, **named_args)
+ end
+ end
+end
diff --git a/fixtures/src_template__api_only/expected/src/serializers/error_serializer.cr b/fixtures/src_template__api_only/expected/src/serializers/error_serializer.cr
new file mode 100644
index 00000000..21a53aa2
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/src/serializers/error_serializer.cr
@@ -0,0 +1,14 @@
+# This is the default error serializer generated by Lucky.
+# Feel free to customize it in any way you like.
+class ErrorSerializer < BaseSerializer
+ def initialize(
+ @message : String,
+ @details : String? = nil,
+ @param : String? = nil # so you can track which param (if any) caused the problem
+ )
+ end
+
+ def render
+ {message: @message, param: @param, details: @details}
+ end
+end
diff --git a/fixtures/src_template__api_only/expected/src/shards.cr b/fixtures/src_template__api_only/expected/src/shards.cr
new file mode 100644
index 00000000..1afc72cb
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/src/shards.cr
@@ -0,0 +1,8 @@
+# Load .env file before any other config or app code
+require "lucky_env"
+LuckyEnv.load?(".env")
+
+# Require your shards here
+require "lucky"
+require "avram/lucky"
+require "carbon"
diff --git a/fixtures/src_template__api_only/expected/src/start_server.cr b/fixtures/src_template__api_only/expected/src/start_server.cr
new file mode 100644
index 00000000..de8af78e
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/src/start_server.cr
@@ -0,0 +1,17 @@
+require "./app"
+
+Habitat.raise_if_missing_settings!
+
+if LuckyEnv.development?
+ Avram::Migrator::Runner.new.ensure_migrated!
+ Avram::SchemaEnforcer.ensure_correct_column_mappings!
+end
+
+app_server = AppServer.new
+puts "Listening on http://#{app_server.host}:#{app_server.port}"
+
+Signal::INT.trap do
+ app_server.close
+end
+
+app_server.listen
diff --git a/fixtures/src_template__api_only/expected/src/test_project.cr b/fixtures/src_template__api_only/expected/src/test_project.cr
new file mode 100644
index 00000000..68e1a8d2
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/src/test_project.cr
@@ -0,0 +1,6 @@
+# Typically you will not use or modify this file. 'shards build' and some
+# other crystal tools will sometimes use this.
+#
+# When this file is compiled/run it will require and run 'start_server',
+# which as its name implies will start the server for you app.
+require "./start_server"
diff --git a/fixtures/src_template__api_only/expected/tasks.cr b/fixtures/src_template__api_only/expected/tasks.cr
new file mode 100644
index 00000000..5a892d4d
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/tasks.cr
@@ -0,0 +1,25 @@
+# This file loads your app and all your tasks when running 'lucky'
+#
+# Run 'lucky --help' to see all available tasks.
+#
+# Learn to create your own tasks:
+# https://luckyframework.org/guides/command-line-tasks/custom-tasks
+
+# See `LuckyEnv#task?`
+ENV["LUCKY_TASK"] = "true"
+
+# Load Lucky and the app (actions, models, etc.)
+require "./src/app"
+require "lucky_task"
+
+# You can add your own tasks here in the ./tasks folder
+require "./tasks/**"
+
+# Load migrations
+require "./db/migrations/**"
+
+# Load Lucky tasks (dev, routes, etc.)
+require "lucky/tasks/**"
+require "avram/lucky/tasks"
+
+LuckyTask::Runner.run
diff --git a/fixtures/src_template__api_only/expected/tasks/.keep b/fixtures/src_template__api_only/expected/tasks/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__api_only/expected/tasks/db/seed/required_data.cr b/fixtures/src_template__api_only/expected/tasks/db/seed/required_data.cr
new file mode 100644
index 00000000..d866040f
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/tasks/db/seed/required_data.cr
@@ -0,0 +1,30 @@
+require "../../../spec/support/factories/**"
+
+# Add seeds here that are *required* for your app to work.
+# For example, you might need at least one admin user or you might need at least
+# one category for your blog posts for the app to work.
+#
+# Use `Db::Seed::SampleData` if your only want to add sample data helpful for
+# development.
+class Db::Seed::RequiredData < LuckyTask::Task
+ summary "Add database records required for the app to work"
+
+ def call
+ # Using a Avram::Factory:
+ #
+ # Use the defaults, but override just the email
+ # UserFactory.create &.email("me@example.com")
+
+ # Using a SaveOperation:
+ #
+ # SaveUser.create!(email: "me@example.com", name: "Jane")
+ #
+ # You likely want to be able to run this file more than once. To do that,
+ # only create the record if it doesn't exist yet:
+ #
+ # unless UserQuery.new.email("me@example.com").first?
+ # SaveUser.create!(email: "me@example.com", name: "Jane")
+ # end
+ puts "Done adding required data"
+ end
+end
diff --git a/fixtures/src_template__api_only/expected/tasks/db/seed/sample_data.cr b/fixtures/src_template__api_only/expected/tasks/db/seed/sample_data.cr
new file mode 100644
index 00000000..231d7e8d
--- /dev/null
+++ b/fixtures/src_template__api_only/expected/tasks/db/seed/sample_data.cr
@@ -0,0 +1,30 @@
+require "../../../spec/support/factories/**"
+
+# Add sample data helpful for development, e.g. (fake users, blog posts, etc.)
+#
+# Use `Db::Seed::RequiredData` if you need to create data *required* for your
+# app to work.
+class Db::Seed::SampleData < LuckyTask::Task
+ summary "Add sample database records helpful for development"
+
+ def call
+ # Using an Avram::Factory:
+ #
+ # Use the defaults, but override just the email
+ # UserFactory.create &.email("me@example.com")
+
+ # Using a SaveOperation:
+ # ```
+ # SignUpUser.create!(email: "me@example.com", password: "test123", password_confirmation: "test123")
+ # ```
+ #
+ # You likely want to be able to run this file more than once. To do that,
+ # only create the record if it doesn't exist yet:
+ # ```
+ # if UserQuery.new.email("me@example.com").none?
+ # SignUpUser.create!(email: "me@example.com", password: "test123", password_confirmation: "test123")
+ # end
+ # ```
+ puts "Done adding sample data"
+ end
+end
diff --git a/fixtures/src_template__generate_auth/expected/.crystal-version b/fixtures/src_template__generate_auth/expected/.crystal-version
new file mode 100644
index 00000000..fdd3be6d
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/.crystal-version
@@ -0,0 +1 @@
+1.6.2
diff --git a/fixtures/src_template__generate_auth/expected/.env b/fixtures/src_template__generate_auth/expected/.env
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__generate_auth/expected/.github/workflows/ci.yml b/fixtures/src_template__generate_auth/expected/.github/workflows/ci.yml
new file mode 100644
index 00000000..278614cf
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/.github/workflows/ci.yml
@@ -0,0 +1,115 @@
+name: test-project CI
+
+on:
+ push:
+ branches: "*"
+ pull_request:
+ branches: "*"
+
+jobs:
+ check-format:
+ strategy:
+ fail-fast: false
+ matrix:
+ crystal_version:
+ - 1.6.2
+ experimental:
+ - false
+ runs-on: ubuntu-latest
+ continue-on-error: ${{ matrix.experimental }}
+ steps:
+ - uses: actions/checkout@v2
+ - name: Install Crystal
+ uses: crystal-lang/install-crystal@v1
+ with:
+ crystal: ${{ matrix.crystal_version }}
+ - name: Format
+ run: crystal tool format --check
+
+ specs:
+ strategy:
+ fail-fast: false
+ matrix:
+ crystal_version:
+ - 1.6.2
+ experimental:
+ - false
+ runs-on: ubuntu-latest
+ env:
+ LUCKY_ENV: test
+ DB_HOST: localhost
+ continue-on-error: ${{ matrix.experimental }}
+ services:
+ postgres:
+ image: postgres:12-alpine
+ env:
+ POSTGRES_PASSWORD: postgres
+ ports:
+ - 5432:5432
+ # Set health checks to wait until postgres has started
+ options: >-
+ --health-cmd pg_isready
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Install Crystal
+ uses: crystal-lang/install-crystal@v1
+ with:
+ crystal: ${{ matrix.crystal_version }}
+
+ - name: Get yarn cache directory path
+ id: yarn-cache-dir-path
+ run: echo "::set-output name=dir::$(yarn cache dir)"
+
+ - name: Set up Yarn cache
+ uses: actions/cache@v2
+ with:
+ path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
+ key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-yarn-
+
+ - name: Set up Node cache
+ uses: actions/cache@v2
+ id: node-cache # use this to check for `cache-hit` (`steps.node-cache.outputs.cache-hit != 'true'`)
+ with:
+ path: '**/node_modules'
+ key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-node-
+ - name: Set up Crystal cache
+ uses: actions/cache@v2
+ id: crystal-cache
+ with:
+ path: |
+ ~/.cache/crystal
+ lib
+ lucky_tasks
+ key: ${{ runner.os }}-crystal-${{ hashFiles('**/shard.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-crystal-
+
+ - name: Install shards
+ if: steps.crystal-cache.outputs.cache-hit != 'true'
+ run: shards check || shards install
+
+ - name: Install yarn packages
+ if: steps.node-cache.outputs.cache-hit != 'true'
+ run: yarn install --frozen-lockfile --no-progress
+ - name: Compiling assets
+ run: yarn prod
+ - name: Build lucky_tasks
+ if: steps.crystal-cache.outputs.cache-hit != 'true'
+ run: crystal build tasks.cr -o ./lucky_tasks
+
+ - name: Prepare database
+ run: |
+ ./lucky_tasks db.create
+ ./lucky_tasks db.migrate
+ ./lucky_tasks db.seed.required_data
+
+ - name: Run tests
+ run: crystal spec
\ No newline at end of file
diff --git a/fixtures/src_template__generate_auth/expected/Procfile b/fixtures/src_template__generate_auth/expected/Procfile
new file mode 100644
index 00000000..e524d70d
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/Procfile
@@ -0,0 +1,2 @@
+web: bin/app
+release: lucky db.migrate
diff --git a/fixtures/src_template__generate_auth/expected/Procfile.dev b/fixtures/src_template__generate_auth/expected/Procfile.dev
new file mode 100644
index 00000000..d9a8173a
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/Procfile.dev
@@ -0,0 +1,3 @@
+system_check: script/system_check && sleep 100000
+web: lucky watch --reload-browser
+assets: yarn watch
diff --git a/fixtures/src_template__generate_auth/expected/README.md b/fixtures/src_template__generate_auth/expected/README.md
new file mode 100644
index 00000000..da2d97cc
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/README.md
@@ -0,0 +1,23 @@
+# test-project
+
+This is a project written using [Lucky](https://luckyframework.org). Enjoy!
+
+### Setting up the project
+
+1. [Install required dependencies](https://luckyframework.org/guides/getting-started/installing#install-required-dependencies)
+1. Update database settings in `config/database.cr`
+1. Run `script/setup`
+1. Run `lucky dev` to start the app
+
+### Using Docker for development
+
+1. [Install Docker](https://docs.docker.com/engine/install/)
+1. Run `docker compose up`
+
+The Docker container will boot all of the necessary components needed to run your Lucky application.
+To configure the container, update the `docker-compose.yml` file, and the `docker/development.dockerfile` file.
+
+
+### Learning Lucky
+
+Lucky uses the [Crystal](https://crystal-lang.org) programming language. You can learn about Lucky from the [Lucky Guides](https://luckyframework.org/guides/getting-started/why-lucky).
diff --git a/fixtures/src_template__generate_auth/expected/config/application.cr b/fixtures/src_template__generate_auth/expected/config/application.cr
new file mode 100644
index 00000000..c807149a
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/config/application.cr
@@ -0,0 +1,24 @@
+# This file may be used for custom Application configurations.
+# It will be loaded before other config files.
+#
+# Read more on configuration:
+# https://luckyframework.org/guides/getting-started/configuration#configuring-your-own-code
+
+# Use this code as an example:
+#
+# ```
+# module Application
+# Habitat.create do
+# setting support_email : String
+# setting lock_with_basic_auth : Bool
+# end
+# end
+#
+# Application.configure do |settings|
+# settings.support_email = "support@myapp.io"
+# settings.lock_with_basic_auth = LuckyEnv.staging?
+# end
+#
+# # In your application, call
+# # `Application.settings.support_email` anywhere you need it.
+# ```
diff --git a/fixtures/src_template__generate_auth/expected/config/colors.cr b/fixtures/src_template__generate_auth/expected/config/colors.cr
new file mode 100644
index 00000000..761ae940
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/config/colors.cr
@@ -0,0 +1,4 @@
+# This enables the color output when in development or test
+# Check out the Colorize docs for more information
+# https://crystal-lang.org/api/Colorize.html
+Colorize.enabled = LuckyEnv.development? || LuckyEnv.test?
diff --git a/fixtures/src_template__generate_auth/expected/config/cookies.cr b/fixtures/src_template__generate_auth/expected/config/cookies.cr
new file mode 100644
index 00000000..2d5055f2
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/config/cookies.cr
@@ -0,0 +1,25 @@
+require "./server"
+
+Lucky::Session.configure do |settings|
+ settings.key = "_test_project_session"
+end
+
+Lucky::CookieJar.configure do |settings|
+ settings.on_set = ->(cookie : HTTP::Cookie) {
+ # If ForceSSLHandler is enabled, only send cookies over HTTPS
+ cookie.secure(Lucky::ForceSSLHandler.settings.enabled)
+
+ # By default, don't allow reading cookies with JavaScript
+ cookie.http_only(true)
+
+ # Restrict cookies to a first-party or same-site context
+ cookie.samesite(:lax)
+
+ # Set all cookies to the root path by default
+ cookie.path("/")
+
+ # You can set other defaults for cookies here. For example:
+ #
+ # cookie.expires(1.year.from_now).domain("mydomain.com")
+ }
+end
diff --git a/fixtures/src_template__generate_auth/expected/config/database.cr b/fixtures/src_template__generate_auth/expected/config/database.cr
new file mode 100644
index 00000000..f614299a
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/config/database.cr
@@ -0,0 +1,29 @@
+database_name = "test_project_#{LuckyEnv.environment}"
+
+AppDatabase.configure do |settings|
+ if LuckyEnv.production?
+ settings.credentials = Avram::Credentials.parse(ENV["DATABASE_URL"])
+ else
+ settings.credentials = Avram::Credentials.parse?(ENV["DATABASE_URL"]?) || Avram::Credentials.new(
+ database: database_name,
+ hostname: ENV["DB_HOST"]? || "localhost",
+ port: ENV["DB_PORT"]?.try(&.to_i) || 5432,
+ # Some common usernames are "postgres", "root", or your system username (run 'whoami')
+ username: ENV["DB_USERNAME"]? || "postgres",
+ # Some Postgres installations require no password. Use "" if that is the case.
+ password: ENV["DB_PASSWORD"]? || "postgres"
+ )
+ end
+end
+
+Avram.configure do |settings|
+ settings.database_to_migrate = AppDatabase
+
+ # In production, allow lazy loading (N+1).
+ # In development and test, raise an error if you forget to preload associations
+ settings.lazy_load_enabled = LuckyEnv.production?
+
+ # Always parse `Time` values with these specific formats.
+ # Used for both database values, and datetime input fields.
+ # settings.time_formats << "%F"
+end
diff --git a/fixtures/src_template__generate_auth/expected/config/email.cr b/fixtures/src_template__generate_auth/expected/config/email.cr
new file mode 100644
index 00000000..7c875449
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/config/email.cr
@@ -0,0 +1,26 @@
+require "carbon_sendgrid_adapter"
+
+BaseEmail.configure do |settings|
+ if LuckyEnv.production?
+ # If you don't need to send emails, set the adapter to DevAdapter instead:
+ #
+ # settings.adapter = Carbon::DevAdapter.new
+ #
+ # If you do need emails, get a key from SendGrid and set an ENV variable
+ send_grid_key = send_grid_key_from_env
+ settings.adapter = Carbon::SendGridAdapter.new(api_key: send_grid_key)
+ elsif LuckyEnv.development?
+ settings.adapter = Carbon::DevAdapter.new(print_emails: true)
+ else
+ settings.adapter = Carbon::DevAdapter.new
+ end
+end
+
+private def send_grid_key_from_env
+ ENV["SEND_GRID_KEY"]? || raise_missing_key_message
+end
+
+private def raise_missing_key_message
+ puts "Missing SEND_GRID_KEY. Set the SEND_GRID_KEY env variable to 'unused' if not sending emails, or set the SEND_GRID_KEY ENV var.".colorize.red
+ exit(1)
+end
diff --git a/fixtures/src_template__generate_auth/expected/config/env.cr b/fixtures/src_template__generate_auth/expected/config/env.cr
new file mode 100644
index 00000000..3f364072
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/config/env.cr
@@ -0,0 +1,33 @@
+# Environments are managed using `LuckyEnv`. By default, development, production
+# and test are supported. See
+# https://luckyframework.org/guides/getting-started/configuration for details.
+#
+# The default environment is development unless the environment variable
+# LUCKY_ENV is set.
+#
+# Example:
+# ```
+# LuckyEnv.environment # => "development"
+# LuckyEnv.development? # => true
+# LuckyEnv.production? # => false
+# LuckyEnv.test? # => false
+# ```
+#
+# New environments can be added using the `LuckyEnv.add_env` macro.
+#
+# Example:
+# ```
+# LuckyEnv.add_env :staging
+# LuckyEnv.staging? # => false
+# ```
+#
+# To determine whether or not a `LuckyTask` is currently running, you can use
+# the `LuckyEnv.task?` predicate.
+#
+# Example:
+# ```
+# LuckyEnv.task? # => false
+# ```
+
+# Add a staging environment.
+# LuckyEnv.add_env :staging
diff --git a/fixtures/src_template__generate_auth/expected/config/error_handler.cr b/fixtures/src_template__generate_auth/expected/config/error_handler.cr
new file mode 100644
index 00000000..c6b736e3
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/config/error_handler.cr
@@ -0,0 +1,3 @@
+Lucky::ErrorHandler.configure do |settings|
+ settings.show_debug_output = !LuckyEnv.production?
+end
diff --git a/fixtures/src_template__generate_auth/expected/config/log.cr b/fixtures/src_template__generate_auth/expected/config/log.cr
new file mode 100644
index 00000000..0e8d7636
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/config/log.cr
@@ -0,0 +1,45 @@
+require "file_utils"
+
+if LuckyEnv.test?
+ # Logs to `tmp/test.log` so you can see what's happening without having
+ # a bunch of log output in your spec results.
+ FileUtils.mkdir_p("tmp")
+
+ backend = Log::IOBackend.new(File.new("tmp/test.log", mode: "w"))
+ backend.formatter = Lucky::PrettyLogFormatter.proc
+ Log.dexter.configure(:debug, backend)
+elsif LuckyEnv.production?
+ # Lucky uses JSON in production so logs can be searched more easily
+ #
+ # If you want logs like in develpoment use 'Lucky::PrettyLogFormatter.proc'.
+ backend = Log::IOBackend.new
+ backend.formatter = Dexter::JSONLogFormatter.proc
+ Log.dexter.configure(:info, backend)
+else
+ # Use a pretty formatter printing to STDOUT in development
+ backend = Log::IOBackend.new
+ backend.formatter = Lucky::PrettyLogFormatter.proc
+ Log.dexter.configure(:debug, backend)
+ DB::Log.level = :info
+end
+
+# Lucky only logs when before/after pipes halt by redirecting, or rendering a
+# response. Pipes that run without halting are not logged.
+#
+# If you want to log every pipe that runs, set the log level to ':info'
+Lucky::ContinuedPipeLog.dexter.configure(:none)
+
+# Lucky only logs failed queries by default.
+#
+# Set the log to ':info' to log all queries
+Avram::QueryLog.dexter.configure(:none)
+
+# Skip logging static assets requests in development
+Lucky::LogHandler.configure do |settings|
+ if LuckyEnv.development?
+ settings.skip_if = ->(context : HTTP::Server::Context) {
+ context.request.method.downcase == "get" &&
+ context.request.resource.starts_with?(/\/css\/|\/js\/|\/assets\/|\/favicon\.ico/)
+ }
+ end
+end
diff --git a/fixtures/src_template__generate_auth/expected/config/route_helper.cr b/fixtures/src_template__generate_auth/expected/config/route_helper.cr
new file mode 100644
index 00000000..ede1f328
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/config/route_helper.cr
@@ -0,0 +1,10 @@
+# This is used when generating URLs for your application
+Lucky::RouteHelper.configure do |settings|
+ if LuckyEnv.production?
+ # Example: https://my_app.com
+ settings.base_uri = ENV.fetch("APP_DOMAIN")
+ else
+ # Set domain to the default host/port in development/test
+ settings.base_uri = "http://localhost:#{Lucky::ServerSettings.port}"
+ end
+end
diff --git a/fixtures/src_template__generate_auth/expected/config/server.cr b/fixtures/src_template__generate_auth/expected/config/server.cr
new file mode 100644
index 00000000..c72314be
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/config/server.cr
@@ -0,0 +1,65 @@
+# Here is where you configure the Lucky server
+#
+# Look at config/route_helper.cr if you want to change the domain used when
+# generating links with `Action.url`.
+Lucky::Server.configure do |settings|
+ if LuckyEnv.production?
+ settings.secret_key_base = secret_key_from_env
+ settings.host = "0.0.0.0"
+ settings.port = ENV["PORT"].to_i
+ settings.gzip_enabled = true
+ # By default certain content types will be gzipped.
+ # For a full list look in
+ # https://github.com/luckyframework/lucky/blob/main/src/lucky/server.cr
+ # To add additional extensions do something like this:
+ # settings.gzip_content_types << "content/type"
+ else
+ settings.secret_key_base = "1234567890"
+ # Change host/port in config/watch.yml
+ # Alternatively, you can set the DEV_PORT env to set the port for local development
+ settings.host = Lucky::ServerSettings.host
+ settings.port = Lucky::ServerSettings.port
+ end
+
+ # By default Lucky will serve static assets in development and production.
+ #
+ # However you could use a CDN when in production like this:
+ #
+ # Lucky::Server.configure do |settings|
+ # if LuckyEnv.production?
+ # settings.asset_host = "https://mycdnhost.com"
+ # else
+ # settings.asset_host = ""
+ # end
+ # end
+ settings.asset_host = "" # Lucky will serve assets
+end
+
+Lucky::ForceSSLHandler.configure do |settings|
+ # To force SSL in production, uncomment the lines below.
+ # This will cause http requests to be redirected to https:
+ #
+ # settings.enabled = LuckyEnv.production?
+ # settings.strict_transport_security = {max_age: 1.year, include_subdomains: true}
+ #
+ # Or, leave it disabled:
+ settings.enabled = false
+end
+
+# Set a unique ID for each HTTP request.
+# To enable the request ID, uncomment the lines below.
+# You can set your own custom String, or use a random UUID.
+# Lucky::RequestIdHandler.configure do |settings|
+# settings.set_request_id = ->(context : HTTP::Server::Context) {
+# UUID.random.to_s
+# }
+# end
+
+private def secret_key_from_env
+ ENV["SECRET_KEY_BASE"]? || raise_missing_secret_key_in_production
+end
+
+private def raise_missing_secret_key_in_production
+ puts "Please set the SECRET_KEY_BASE environment variable. You can generate a secret key with 'lucky gen.secret_key'".colorize.red
+ exit(1)
+end
diff --git a/fixtures/src_template__generate_auth/expected/config/watch.yml b/fixtures/src_template__generate_auth/expected/config/watch.yml
new file mode 100644
index 00000000..3a59b410
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/config/watch.yml
@@ -0,0 +1,3 @@
+host: 127.0.0.1
+port: 3000
+reload_port: 3001
diff --git a/fixtures/src_template__generate_auth/expected/db/migrations/.keep b/fixtures/src_template__generate_auth/expected/db/migrations/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__generate_auth/expected/docker-compose.yml b/fixtures/src_template__generate_auth/expected/docker-compose.yml
new file mode 100644
index 00000000..d00779db
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/docker-compose.yml
@@ -0,0 +1,45 @@
+version: "3.8"
+services:
+ lucky:
+ build:
+ context: .
+ dockerfile: docker/development.dockerfile
+ environment:
+ DATABASE_URL: postgres://lucky:password@postgres:5432/lucky
+ DEV_HOST: "0.0.0.0"
+ volumes:
+ - .:/app
+ - node_modules:/app/node_modules
+ - shards_lib:/app/lib
+ - app_bin:/app/bin
+ - build_cache:/root/.cache
+ depends_on:
+ - postgres
+ ports:
+ - 3000:3000 # This is the Lucky Server port
+ - 3001:3001 # This is the Lucky watcher reload port
+
+ entrypoint: ["docker/dev_entrypoint.sh"]
+
+ postgres:
+ image: postgres:14-alpine
+ environment:
+ POSTGRES_USER: lucky
+ POSTGRES_PASSWORD: password
+ POSTGRES_DB: lucky
+ volumes:
+ - postgres_data:/var/lib/postgresql/data
+ ports:
+ # The postgres database container is exposed on the host at port 6543 to
+ # allow connecting directly to it with postgres clients. The port differs
+ # from the postgres default to avoid conflict with existing postgres
+ # servers. Connect to a running postgres container with:
+ # postgres://lucky:password@localhost:6543/lucky
+ - 6543:5432
+
+volumes:
+ postgres_data:
+ node_modules:
+ shards_lib:
+ app_bin:
+ build_cache:
diff --git a/fixtures/src_template__generate_auth/expected/docker/dev_entrypoint.sh b/fixtures/src_template__generate_auth/expected/docker/dev_entrypoint.sh
new file mode 100755
index 00000000..d16ef6ab
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/docker/dev_entrypoint.sh
@@ -0,0 +1,46 @@
+#!/bin/bash
+
+set -euo pipefail
+
+# This is the entrypoint script used for development docker workflows.
+# By default it will:
+# - Install dependencies.
+# - Run migrations.
+# - Start the dev server.
+# It also accepts any commands to be run instead.
+
+
+warnfail () {
+ echo "$@" >&2
+ exit 1
+}
+
+case ${1:-} in
+ "") # If no arguments are provided, start lucky dev server.
+ ;;
+
+ *) # If any arguments are provided, execute them instead.
+ exec "$@"
+esac
+
+if ! [ -d bin ] ; then
+ echo 'Creating bin directory'
+ mkdir bin
+fi
+echo 'Installing npm packages...'
+yarn install
+if ! shards check ; then
+ echo 'Installing shards...'
+ shards install
+fi
+
+echo 'Waiting for postgres to be available...'
+./docker/wait-for-it.sh -q postgres:5432
+
+if ! psql -d "$DATABASE_URL" -c '\d migrations' > /dev/null ; then
+ echo 'Finishing database setup...'
+ lucky db.migrate
+fi
+
+echo 'Starting lucky dev server...'
+exec lucky dev
diff --git a/fixtures/src_template__generate_auth/expected/docker/development.dockerfile b/fixtures/src_template__generate_auth/expected/docker/development.dockerfile
new file mode 100644
index 00000000..be3dba5f
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/docker/development.dockerfile
@@ -0,0 +1,34 @@
+FROM crystallang/crystal:1.6.2
+
+# Install utilities required to make this Dockerfile run
+RUN apt-get update && \
+ apt-get install -y wget
+# Add the nodesource ppa to apt. Update this to change the nodejs version.
+RUN wget https://deb.nodesource.com/setup_16.x -O- | bash
+
+# Apt installs:
+# - nodejs (from above ppa) is required for front-end apps.
+# - Postgres cli tools are required for lucky-cli.
+# - tmux is required for the Overmind process manager.
+RUN apt-get update && \
+ apt-get install -y nodejs postgresql-client tmux && \
+ rm -rf /var/lib/apt/lists/*
+
+# NPM global installs:
+# - Yarn is the default package manager for the node component of a lucky
+# browser app.
+# - Mix is the default asset compiler.
+RUN npm install -g yarn mix
+
+# Install lucky cli
+WORKDIR /lucky/cli
+RUN git clone https://github.com/luckyframework/lucky_cli . && \
+ git checkout v1.0.0 && \
+ shards build --without-development && \
+ cp bin/lucky /usr/bin
+
+WORKDIR /app
+ENV DATABASE_URL=postgres://postgres:postgres@host.docker.internal:5432/postgres
+EXPOSE 3000
+EXPOSE 3001
+
diff --git a/fixtures/src_template__generate_auth/expected/docker/wait-for-it.sh b/fixtures/src_template__generate_auth/expected/docker/wait-for-it.sh
new file mode 100755
index 00000000..06e0638c
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/docker/wait-for-it.sh
@@ -0,0 +1,189 @@
+#!/usr/bin/bash
+#
+# Pulled from https://github.com/vishnubob/wait-for-it on 2022-02-28.
+# Licensed under the MIT license as of 81b1373f.
+#
+# Below this line, wait-for-it is the original work of the author.
+#
+# Use this script to test if a given TCP host/port are available
+
+WAITFORIT_cmdname=${0##*/}
+
+echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }
+
+usage()
+{
+ cat << USAGE >&2
+Usage:
+ $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args]
+ -h HOST | --host=HOST Host or IP under test
+ -p PORT | --port=PORT TCP port under test
+ Alternatively, you specify the host and port as host:port
+ -s | --strict Only execute subcommand if the test succeeds
+ -q | --quiet Don't output any status messages
+ -t TIMEOUT | --timeout=TIMEOUT
+ Timeout in seconds, zero for no timeout
+ -- COMMAND ARGS Execute command with args after the test finishes
+USAGE
+ exit 1
+}
+
+wait_for()
+{
+ if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
+ echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
+ else
+ echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout"
+ fi
+ WAITFORIT_start_ts=$(date +%s)
+ while :
+ do
+ if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then
+ nc -z $WAITFORIT_HOST $WAITFORIT_PORT
+ WAITFORIT_result=$?
+ else
+ (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1
+ WAITFORIT_result=$?
+ fi
+ if [[ $WAITFORIT_result -eq 0 ]]; then
+ WAITFORIT_end_ts=$(date +%s)
+ echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds"
+ break
+ fi
+ sleep 1
+ done
+ return $WAITFORIT_result
+}
+
+wait_for_wrapper()
+{
+ # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
+ if [[ $WAITFORIT_QUIET -eq 1 ]]; then
+ timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
+ else
+ timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
+ fi
+ WAITFORIT_PID=$!
+ trap "kill -INT -$WAITFORIT_PID" INT
+ wait $WAITFORIT_PID
+ WAITFORIT_RESULT=$?
+ if [[ $WAITFORIT_RESULT -ne 0 ]]; then
+ echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
+ fi
+ return $WAITFORIT_RESULT
+}
+
+# process arguments
+while [[ $# -gt 0 ]]
+do
+ case "$1" in
+ *:* )
+ WAITFORIT_hostport=(${1//:/ })
+ WAITFORIT_HOST=${WAITFORIT_hostport[0]}
+ WAITFORIT_PORT=${WAITFORIT_hostport[1]}
+ shift 1
+ ;;
+ --child)
+ WAITFORIT_CHILD=1
+ shift 1
+ ;;
+ -q | --quiet)
+ WAITFORIT_QUIET=1
+ shift 1
+ ;;
+ -s | --strict)
+ WAITFORIT_STRICT=1
+ shift 1
+ ;;
+ -h)
+ WAITFORIT_HOST="$2"
+ if [[ $WAITFORIT_HOST == "" ]]; then break; fi
+ shift 2
+ ;;
+ --host=*)
+ WAITFORIT_HOST="${1#*=}"
+ shift 1
+ ;;
+ -p)
+ WAITFORIT_PORT="$2"
+ if [[ $WAITFORIT_PORT == "" ]]; then break; fi
+ shift 2
+ ;;
+ --port=*)
+ WAITFORIT_PORT="${1#*=}"
+ shift 1
+ ;;
+ -t)
+ WAITFORIT_TIMEOUT="$2"
+ if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi
+ shift 2
+ ;;
+ --timeout=*)
+ WAITFORIT_TIMEOUT="${1#*=}"
+ shift 1
+ ;;
+ --)
+ shift
+ WAITFORIT_CLI=("$@")
+ break
+ ;;
+ --help)
+ usage
+ ;;
+ *)
+ echoerr "Unknown argument: $1"
+ usage
+ ;;
+ esac
+done
+
+if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then
+ echoerr "Error: you need to provide a host and port to test."
+ usage
+fi
+
+WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15}
+WAITFORIT_STRICT=${WAITFORIT_STRICT:-0}
+WAITFORIT_CHILD=${WAITFORIT_CHILD:-0}
+WAITFORIT_QUIET=${WAITFORIT_QUIET:-0}
+
+# Check to see if timeout is from busybox?
+WAITFORIT_TIMEOUT_PATH=$(type -p timeout)
+WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH)
+
+WAITFORIT_BUSYTIMEFLAG=""
+if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then
+ WAITFORIT_ISBUSY=1
+ # Check if busybox timeout uses -t flag
+ # (recent Alpine versions don't support -t anymore)
+ if timeout &>/dev/stdout | grep -q -e '-t '; then
+ WAITFORIT_BUSYTIMEFLAG="-t"
+ fi
+else
+ WAITFORIT_ISBUSY=0
+fi
+
+if [[ $WAITFORIT_CHILD -gt 0 ]]; then
+ wait_for
+ WAITFORIT_RESULT=$?
+ exit $WAITFORIT_RESULT
+else
+ if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
+ wait_for_wrapper
+ WAITFORIT_RESULT=$?
+ else
+ wait_for
+ WAITFORIT_RESULT=$?
+ fi
+fi
+
+if [[ $WAITFORIT_CLI != "" ]]; then
+ if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then
+ echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess"
+ exit $WAITFORIT_RESULT
+ fi
+ exec "${WAITFORIT_CLI[@]}"
+else
+ exit $WAITFORIT_RESULT
+fi
+
diff --git a/fixtures/src_template__generate_auth/expected/script/helpers/function_helpers b/fixtures/src_template__generate_auth/expected/script/helpers/function_helpers
new file mode 100644
index 00000000..388fa67e
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/script/helpers/function_helpers
@@ -0,0 +1,67 @@
+#!/usr/bin/env bash
+
+# This file contains a set of functions used as helpers
+# for various tasks. Read the examples for each one for
+# more information. Feel free to put any additional helper
+# functions you may need for your app
+
+
+# Returns true if the command $1 is not found
+# example:
+# if command_not_found "yarn"; then
+# echo "no yarn"
+# fi
+command_not_found() {
+ ! command -v $1 > /dev/null
+ return $?
+}
+
+# Returns true if the command $1 is not running
+# You must supply the full command to check as an argument
+# example:
+# if command_not_running "redis-cli ping"; then
+# print_error "Redis is not running"
+# fi
+command_not_running() {
+ $1
+ if [ $? -ne 0 ]; then
+ true
+ else
+ false
+ fi
+}
+
+# Returns true if the OS is macOS
+# example:
+# if is_mac; then
+# echo "do mac stuff"
+# fi
+is_mac() {
+ if [[ "$OSTYPE" == "darwin"* ]]; then
+ true
+ else
+ false
+ fi
+}
+
+# Returns true if the OS is linux based
+# example:
+# if is_linux; then
+# echo "do linux stuff"
+# fi
+is_linux() {
+ if [[ "$OSTYPE" == "linux"* ]]; then
+ true
+ else
+ false
+ fi
+}
+
+# Prints error and exit.
+# example:
+# print_error "Redis is not running. Run it with some_command"
+print_error() {
+ printf "${BOLD_RED_COLOR}There is a problem with your system setup:\n\n"
+ printf "${BOLD_RED_COLOR}$1 \n\n" | indent
+ exit 1
+}
diff --git a/fixtures/src_template__generate_auth/expected/script/helpers/text_helpers b/fixtures/src_template__generate_auth/expected/script/helpers/text_helpers
new file mode 100644
index 00000000..34b77a8c
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/script/helpers/text_helpers
@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+
+# This file contains a set of functions used to format text,
+# and make printing text a little easier. Feel free to put
+# any additional functions you need for formatting your shell
+# output text.
+
+# Colors
+BOLD_RED_COLOR="\e[1m\e[31m"
+
+# Indents the text 2 spaces
+# example:
+# printf "Hello" | indent
+indent() {
+ while read LINE; do
+ echo " $LINE" || true
+ done
+}
+
+# Prints out an arrow to your custom notice
+# example:
+# notice "Installing new magic"
+notice() {
+ printf "\n▸ $1\n"
+}
+
+# Prints out a check mark and Done.
+# example:
+# print_done
+print_done() {
+ printf "✔ Done\n" | indent
+}
diff --git a/fixtures/src_template__generate_auth/expected/script/setup b/fixtures/src_template__generate_auth/expected/script/setup
new file mode 100755
index 00000000..bf0bc317
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/script/setup
@@ -0,0 +1,45 @@
+#!/usr/bin/env bash
+
+# Exit if any subcommand fails
+set -e
+set -o pipefail
+
+source script/helpers/text_helpers
+
+
+notice "Running System Check"
+./script/system_check
+print_done
+
+notice "Installing node dependencies"
+yarn install --no-progress | indent
+
+notice "Compiling assets"
+yarn dev | indent
+
+print_done
+
+notice "Installing shards"
+shards install --ignore-crystal-version | indent
+
+if [ ! -f ".env" ]; then
+ notice "No .env found. Creating one."
+ touch .env
+ print_done
+fi
+
+notice "Creating the database"
+lucky db.create | indent
+
+notice "Verifying postgres connection"
+lucky db.verify_connection | indent
+
+notice "Migrating the database"
+lucky db.migrate | indent
+
+notice "Seeding the database with required and sample records"
+lucky db.seed.required_data | indent
+lucky db.seed.sample_data | indent
+
+print_done
+notice "Run 'lucky dev' to start the app"
diff --git a/fixtures/src_template__generate_auth/expected/script/system_check b/fixtures/src_template__generate_auth/expected/script/system_check
new file mode 100755
index 00000000..c27c926b
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/script/system_check
@@ -0,0 +1,37 @@
+#!/usr/bin/env bash
+
+source script/helpers/text_helpers
+source script/helpers/function_helpers
+
+# Use this script to check the system for required tools and process that your app needs.
+# A few helper functions are provided to make writing bash a little easier. See the
+# script/helpers/function_helpers file for more examples.
+#
+# A few examples you might use here:
+# * 'lucky db.verify_connection' to test postgres can be connected
+# * Checking that elasticsearch, redis, or postgres is installed and/or booted
+# * Note: Booting additional processes for things like mail, background jobs, etc...
+# should go in your Procfile.dev.
+
+if command_not_found "yarn"; then
+ print_error "Yarn is not installed\n See https://yarnpkg.com/lang/en/docs/install/ for install instructions."
+fi
+
+if command_not_found "createdb"; then
+ MSG="Please install the postgres CLI tools, then try again."
+ if is_mac; then
+ MSG="$MSG\nIf you're using Postgres.app, see https://postgresapp.com/documentation/cli-tools.html."
+ fi
+ MSG="$MSG\nSee https://www.postgresql.org/docs/current/tutorial-install.html for install instructions."
+
+ print_error "$MSG"
+fi
+
+
+## CUSTOM PRE-BOOT CHECKS ##
+# example:
+# if command_not_running "redis-cli ping"; then
+# print_error "Redis is not running."
+# fi
+
+
diff --git a/fixtures/src_template__generate_auth/expected/spec/setup/clean_database.cr b/fixtures/src_template__generate_auth/expected/spec/setup/clean_database.cr
new file mode 100644
index 00000000..a1bc631c
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/spec/setup/clean_database.cr
@@ -0,0 +1,3 @@
+Spec.before_each do
+ AppDatabase.truncate
+end
diff --git a/fixtures/src_template__generate_auth/expected/spec/setup/reset_emails.cr b/fixtures/src_template__generate_auth/expected/spec/setup/reset_emails.cr
new file mode 100644
index 00000000..140ab416
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/spec/setup/reset_emails.cr
@@ -0,0 +1,3 @@
+Spec.before_each do
+ Carbon::DevAdapter.reset
+end
diff --git a/fixtures/src_template__generate_auth/expected/spec/setup/setup_database.cr b/fixtures/src_template__generate_auth/expected/spec/setup/setup_database.cr
new file mode 100644
index 00000000..393c6da3
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/spec/setup/setup_database.cr
@@ -0,0 +1,2 @@
+Db::Create.new(quiet: true).call
+Db::Migrate.new(quiet: true).call
diff --git a/fixtures/src_template__generate_auth/expected/spec/setup/start_app_server.cr b/fixtures/src_template__generate_auth/expected/spec/setup/start_app_server.cr
new file mode 100644
index 00000000..ff0bfeeb
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/spec/setup/start_app_server.cr
@@ -0,0 +1,10 @@
+app_server = AppServer.new
+
+spawn do
+ app_server.listen
+end
+
+Spec.after_suite do
+ LuckyFlow.shutdown
+ app_server.close
+end
diff --git a/fixtures/src_template__generate_auth/expected/spec/spec_helper.cr b/fixtures/src_template__generate_auth/expected/spec/spec_helper.cr
new file mode 100644
index 00000000..93914648
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/spec/spec_helper.cr
@@ -0,0 +1,26 @@
+ENV["LUCKY_ENV"] = "test"
+ENV["DEV_PORT"] = "5001"
+require "spec"
+require "lucky_flow"
+require "lucky_flow/ext/lucky"
+require "lucky_flow/ext/avram"
+
+require "lucky_flow/ext/authentic"
+require "../src/app"
+require "./support/flows/base_flow"
+require "./support/**"
+require "../db/migrations/**"
+
+# Add/modify files in spec/setup to start/configure programs or run hooks
+#
+# By default there are scripts for setting up and cleaning the database,
+# configuring LuckyFlow, starting the app server, etc.
+require "./setup/**"
+
+include Carbon::Expectations
+include Lucky::RequestExpectations
+include LuckyFlow::Expectations
+
+Avram::Migrator::Runner.new.ensure_migrated!
+Avram::SchemaEnforcer.ensure_correct_column_mappings!
+Habitat.raise_if_missing_settings!
diff --git a/fixtures/src_template__generate_auth/expected/spec/support/.keep b/fixtures/src_template__generate_auth/expected/spec/support/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__generate_auth/expected/spec/support/api_client.cr b/fixtures/src_template__generate_auth/expected/spec/support/api_client.cr
new file mode 100644
index 00000000..46d449a8
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/spec/support/api_client.cr
@@ -0,0 +1,12 @@
+class ApiClient < Lucky::BaseHTTPClient
+ app AppServer.new
+
+ def initialize
+ super
+ headers("Content-Type": "application/json")
+ end
+
+ def self.auth(user : User)
+ new.headers("Authorization": UserToken.generate(user))
+ end
+end
diff --git a/fixtures/src_template__generate_auth/expected/spec/support/factories/.keep b/fixtures/src_template__generate_auth/expected/spec/support/factories/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__generate_auth/expected/src/actions/api_action.cr b/fixtures/src_template__generate_auth/expected/src/actions/api_action.cr
new file mode 100644
index 00000000..a16fd09e
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/src/actions/api_action.cr
@@ -0,0 +1,17 @@
+# Include modules and add methods that are for all API requests
+abstract class ApiAction < Lucky::Action
+ # APIs typically do not need to send cookie/session data.
+ # Remove this line if you want to send cookies in the response header.
+ disable_cookies
+ accepted_formats [:json]
+
+ include Api::Auth::Helpers
+
+ # By default all actions require sign in.
+ # Add 'include Api::Auth::SkipRequireAuthToken' to your actions to allow all requests.
+ include Api::Auth::RequireAuthToken
+
+ # By default all actions are required to use underscores to separate words.
+ # Add 'include Lucky::SkipRouteStyleCheck' to your actions if you wish to ignore this check for specific routes.
+ include Lucky::EnforceUnderscoredRoute
+end
diff --git a/fixtures/src_template__generate_auth/expected/src/actions/errors/show.cr b/fixtures/src_template__generate_auth/expected/src/actions/errors/show.cr
new file mode 100644
index 00000000..d01ed541
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/src/actions/errors/show.cr
@@ -0,0 +1,63 @@
+# This class handles error responses and reporting.
+#
+# https://luckyframework.org/guides/http-and-routing/error-handling
+class Errors::Show < Lucky::ErrorAction
+ DEFAULT_MESSAGE = "Something went wrong."
+ default_format :html
+ dont_report [Lucky::RouteNotFoundError, Avram::RecordNotFoundError]
+
+ def render(error : Lucky::RouteNotFoundError | Avram::RecordNotFoundError)
+ if html?
+ error_html "Sorry, we couldn't find that page.", status: 404
+ else
+ error_json "Not found", status: 404
+ end
+ end
+
+ # When the request is JSON and an InvalidOperationError is raised, show a
+ # helpful error with the param that is invalid, and what was wrong with it.
+ def render(error : Avram::InvalidOperationError)
+ if html?
+ error_html DEFAULT_MESSAGE, status: 500
+ else
+ error_json \
+ message: error.renderable_message,
+ details: error.renderable_details,
+ param: error.invalid_attribute_name,
+ status: 400
+ end
+ end
+
+ # Always keep this below other 'render' methods or it may override your
+ # custom 'render' methods.
+ def render(error : Lucky::RenderableError)
+ if html?
+ error_html DEFAULT_MESSAGE, status: error.renderable_status
+ else
+ error_json error.renderable_message, status: error.renderable_status
+ end
+ end
+
+ # If none of the 'render' methods return a response for the raised Exception,
+ # Lucky will use this method.
+ def default_render(error : Exception) : Lucky::Response
+ if html?
+ error_html DEFAULT_MESSAGE, status: 500
+ else
+ error_json DEFAULT_MESSAGE, status: 500
+ end
+ end
+
+ private def error_html(message : String, status : Int)
+ context.response.status_code = status
+ html_with_status Errors::ShowPage, status, message: message, status_code: status
+ end
+
+ private def error_json(message : String, status : Int, details = nil, param = nil)
+ json ErrorSerializer.new(message: message, details: details, param: param), status: status
+ end
+
+ private def report(error : Exception) : Nil
+ # Send to Rollbar, send an email, etc.
+ end
+end
diff --git a/fixtures/src_template__generate_auth/expected/src/actions/home/index.cr b/fixtures/src_template__generate_auth/expected/src/actions/home/index.cr
new file mode 100644
index 00000000..f780130a
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/src/actions/home/index.cr
@@ -0,0 +1,18 @@
+class Home::Index < BrowserAction
+ include Auth::AllowGuests
+
+ get "/" do
+ if current_user?
+ redirect Me::Show
+ else
+ # When you're ready change this line to:
+ #
+ # redirect SignIns::New
+ #
+ # Or maybe show signed out users a marketing page:
+ #
+ # html Marketing::IndexPage
+ html Lucky::WelcomePage
+ end
+ end
+end
diff --git a/fixtures/src_template__generate_auth/expected/src/actions/mixins/.keep b/fixtures/src_template__generate_auth/expected/src/actions/mixins/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__generate_auth/expected/src/app.cr b/fixtures/src_template__generate_auth/expected/src/app.cr
new file mode 100644
index 00000000..9d3ee3c7
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/src/app.cr
@@ -0,0 +1,26 @@
+require "./shards"
+
+# Load the asset manifest
+Lucky::AssetHelpers.load_manifest "public/mix-manifest.json"
+
+require "../config/server"
+require "./app_database"
+require "../config/**"
+require "./models/base_model"
+require "./models/mixins/**"
+require "./models/**"
+require "./queries/mixins/**"
+require "./queries/**"
+require "./operations/mixins/**"
+require "./operations/**"
+require "./serializers/base_serializer"
+require "./serializers/**"
+require "./emails/base_email"
+require "./emails/**"
+require "./actions/mixins/**"
+require "./actions/**"
+require "./components/base_component"
+require "./components/**"
+require "./pages/**"
+require "../db/migrations/**"
+require "./app_server"
diff --git a/fixtures/src_template__generate_auth/expected/src/app_database.cr b/fixtures/src_template__generate_auth/expected/src/app_database.cr
new file mode 100644
index 00000000..0efd4f50
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/src/app_database.cr
@@ -0,0 +1,2 @@
+class AppDatabase < Avram::Database
+end
diff --git a/fixtures/src_template__generate_auth/expected/src/app_server.cr b/fixtures/src_template__generate_auth/expected/src/app_server.cr
new file mode 100644
index 00000000..8ec16c3c
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/src/app_server.cr
@@ -0,0 +1,26 @@
+class AppServer < Lucky::BaseAppServer
+ # Learn about middleware with HTTP::Handlers:
+ # https://luckyframework.org/guides/http-and-routing/http-handlers
+ def middleware : Array(HTTP::Handler)
+ [
+ Lucky::RequestIdHandler.new,
+ Lucky::ForceSSLHandler.new,
+ Lucky::HttpMethodOverrideHandler.new,
+ Lucky::LogHandler.new,
+ Lucky::ErrorHandler.new(action: Errors::Show),
+ Lucky::RemoteIpHandler.new,
+ Lucky::RouteHandler.new,
+ Lucky::StaticCompressionHandler.new("./public", file_ext: "gz", content_encoding: "gzip"),
+ Lucky::StaticFileHandler.new("./public", fallthrough: false, directory_listing: false),
+ Lucky::RouteNotFoundHandler.new,
+ ] of HTTP::Handler
+ end
+
+ def protocol
+ "http"
+ end
+
+ def listen
+ server.listen(host, port, reuse_port: false)
+ end
+end
diff --git a/fixtures/src_template__generate_auth/expected/src/emails/base_email.cr b/fixtures/src_template__generate_auth/expected/src/emails/base_email.cr
new file mode 100644
index 00000000..656f4f11
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/src/emails/base_email.cr
@@ -0,0 +1,15 @@
+# Learn about sending emails
+# https://luckyframework.org/guides/emails/sending-emails-with-carbon
+abstract class BaseEmail < Carbon::Email
+ # You can add defaults using the 'inherited' hook
+ #
+ # Example:
+ #
+ # macro inherited
+ # from default_from
+ # end
+ #
+ # def default_from
+ # Carbon::Address.new("support@app.com")
+ # end
+end
diff --git a/fixtures/src_template__generate_auth/expected/src/models/base_model.cr b/fixtures/src_template__generate_auth/expected/src/models/base_model.cr
new file mode 100644
index 00000000..6bafeb84
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/src/models/base_model.cr
@@ -0,0 +1,5 @@
+abstract class BaseModel < Avram::Model
+ def self.database : Avram::Database.class
+ AppDatabase
+ end
+end
diff --git a/fixtures/src_template__generate_auth/expected/src/models/mixins/.keep b/fixtures/src_template__generate_auth/expected/src/models/mixins/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__generate_auth/expected/src/operations/.keep b/fixtures/src_template__generate_auth/expected/src/operations/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__generate_auth/expected/src/operations/mixins/.keep b/fixtures/src_template__generate_auth/expected/src/operations/mixins/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__generate_auth/expected/src/queries/.keep b/fixtures/src_template__generate_auth/expected/src/queries/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__generate_auth/expected/src/queries/mixins/.keep b/fixtures/src_template__generate_auth/expected/src/queries/mixins/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__generate_auth/expected/src/serializers/.keep b/fixtures/src_template__generate_auth/expected/src/serializers/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__generate_auth/expected/src/serializers/base_serializer.cr b/fixtures/src_template__generate_auth/expected/src/serializers/base_serializer.cr
new file mode 100644
index 00000000..3ad0a669
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/src/serializers/base_serializer.cr
@@ -0,0 +1,7 @@
+abstract class BaseSerializer < Lucky::Serializer
+ def self.for_collection(collection : Enumerable, *args, **named_args)
+ collection.map do |object|
+ new(object, *args, **named_args)
+ end
+ end
+end
diff --git a/fixtures/src_template__generate_auth/expected/src/serializers/error_serializer.cr b/fixtures/src_template__generate_auth/expected/src/serializers/error_serializer.cr
new file mode 100644
index 00000000..21a53aa2
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/src/serializers/error_serializer.cr
@@ -0,0 +1,14 @@
+# This is the default error serializer generated by Lucky.
+# Feel free to customize it in any way you like.
+class ErrorSerializer < BaseSerializer
+ def initialize(
+ @message : String,
+ @details : String? = nil,
+ @param : String? = nil # so you can track which param (if any) caused the problem
+ )
+ end
+
+ def render
+ {message: @message, param: @param, details: @details}
+ end
+end
diff --git a/fixtures/src_template__generate_auth/expected/src/shards.cr b/fixtures/src_template__generate_auth/expected/src/shards.cr
new file mode 100644
index 00000000..7cadec18
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/src/shards.cr
@@ -0,0 +1,10 @@
+# Load .env file before any other config or app code
+require "lucky_env"
+LuckyEnv.load?(".env")
+
+# Require your shards here
+require "lucky"
+require "avram/lucky"
+require "carbon"
+require "authentic"
+require "jwt"
diff --git a/fixtures/src_template__generate_auth/expected/src/start_server.cr b/fixtures/src_template__generate_auth/expected/src/start_server.cr
new file mode 100644
index 00000000..9df5d1fd
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/src/start_server.cr
@@ -0,0 +1,16 @@
+require "./app"
+
+Habitat.raise_if_missing_settings!
+
+if LuckyEnv.development?
+ Avram::Migrator::Runner.new.ensure_migrated!
+ Avram::SchemaEnforcer.ensure_correct_column_mappings!
+end
+
+app_server = AppServer.new
+
+Signal::INT.trap do
+ app_server.close
+end
+
+app_server.listen
diff --git a/fixtures/src_template__generate_auth/expected/src/test_project.cr b/fixtures/src_template__generate_auth/expected/src/test_project.cr
new file mode 100644
index 00000000..68e1a8d2
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/src/test_project.cr
@@ -0,0 +1,6 @@
+# Typically you will not use or modify this file. 'shards build' and some
+# other crystal tools will sometimes use this.
+#
+# When this file is compiled/run it will require and run 'start_server',
+# which as its name implies will start the server for you app.
+require "./start_server"
diff --git a/fixtures/src_template__generate_auth/expected/tasks.cr b/fixtures/src_template__generate_auth/expected/tasks.cr
new file mode 100644
index 00000000..5a892d4d
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/tasks.cr
@@ -0,0 +1,25 @@
+# This file loads your app and all your tasks when running 'lucky'
+#
+# Run 'lucky --help' to see all available tasks.
+#
+# Learn to create your own tasks:
+# https://luckyframework.org/guides/command-line-tasks/custom-tasks
+
+# See `LuckyEnv#task?`
+ENV["LUCKY_TASK"] = "true"
+
+# Load Lucky and the app (actions, models, etc.)
+require "./src/app"
+require "lucky_task"
+
+# You can add your own tasks here in the ./tasks folder
+require "./tasks/**"
+
+# Load migrations
+require "./db/migrations/**"
+
+# Load Lucky tasks (dev, routes, etc.)
+require "lucky/tasks/**"
+require "avram/lucky/tasks"
+
+LuckyTask::Runner.run
diff --git a/fixtures/src_template__generate_auth/expected/tasks/.keep b/fixtures/src_template__generate_auth/expected/tasks/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__generate_auth/expected/tasks/db/seed/required_data.cr b/fixtures/src_template__generate_auth/expected/tasks/db/seed/required_data.cr
new file mode 100644
index 00000000..d866040f
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/tasks/db/seed/required_data.cr
@@ -0,0 +1,30 @@
+require "../../../spec/support/factories/**"
+
+# Add seeds here that are *required* for your app to work.
+# For example, you might need at least one admin user or you might need at least
+# one category for your blog posts for the app to work.
+#
+# Use `Db::Seed::SampleData` if your only want to add sample data helpful for
+# development.
+class Db::Seed::RequiredData < LuckyTask::Task
+ summary "Add database records required for the app to work"
+
+ def call
+ # Using a Avram::Factory:
+ #
+ # Use the defaults, but override just the email
+ # UserFactory.create &.email("me@example.com")
+
+ # Using a SaveOperation:
+ #
+ # SaveUser.create!(email: "me@example.com", name: "Jane")
+ #
+ # You likely want to be able to run this file more than once. To do that,
+ # only create the record if it doesn't exist yet:
+ #
+ # unless UserQuery.new.email("me@example.com").first?
+ # SaveUser.create!(email: "me@example.com", name: "Jane")
+ # end
+ puts "Done adding required data"
+ end
+end
diff --git a/fixtures/src_template__generate_auth/expected/tasks/db/seed/sample_data.cr b/fixtures/src_template__generate_auth/expected/tasks/db/seed/sample_data.cr
new file mode 100644
index 00000000..231d7e8d
--- /dev/null
+++ b/fixtures/src_template__generate_auth/expected/tasks/db/seed/sample_data.cr
@@ -0,0 +1,30 @@
+require "../../../spec/support/factories/**"
+
+# Add sample data helpful for development, e.g. (fake users, blog posts, etc.)
+#
+# Use `Db::Seed::RequiredData` if you need to create data *required* for your
+# app to work.
+class Db::Seed::SampleData < LuckyTask::Task
+ summary "Add sample database records helpful for development"
+
+ def call
+ # Using an Avram::Factory:
+ #
+ # Use the defaults, but override just the email
+ # UserFactory.create &.email("me@example.com")
+
+ # Using a SaveOperation:
+ # ```
+ # SignUpUser.create!(email: "me@example.com", password: "test123", password_confirmation: "test123")
+ # ```
+ #
+ # You likely want to be able to run this file more than once. To do that,
+ # only create the record if it doesn't exist yet:
+ # ```
+ # if UserQuery.new.email("me@example.com").none?
+ # SignUpUser.create!(email: "me@example.com", password: "test123", password_confirmation: "test123")
+ # end
+ # ```
+ puts "Done adding sample data"
+ end
+end
diff --git a/fixtures/src_template__sec_tester/expected/.crystal-version b/fixtures/src_template__sec_tester/expected/.crystal-version
new file mode 100644
index 00000000..fdd3be6d
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/.crystal-version
@@ -0,0 +1 @@
+1.6.2
diff --git a/fixtures/src_template__sec_tester/expected/.env b/fixtures/src_template__sec_tester/expected/.env
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__sec_tester/expected/.github/workflows/ci.yml b/fixtures/src_template__sec_tester/expected/.github/workflows/ci.yml
new file mode 100644
index 00000000..9b10bbcb
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/.github/workflows/ci.yml
@@ -0,0 +1,118 @@
+name: test-project CI
+
+on:
+ push:
+ branches: "*"
+ pull_request:
+ branches: "*"
+
+jobs:
+ check-format:
+ strategy:
+ fail-fast: false
+ matrix:
+ crystal_version:
+ - 1.6.2
+ experimental:
+ - false
+ runs-on: ubuntu-latest
+ continue-on-error: ${{ matrix.experimental }}
+ steps:
+ - uses: actions/checkout@v2
+ - name: Install Crystal
+ uses: crystal-lang/install-crystal@v1
+ with:
+ crystal: ${{ matrix.crystal_version }}
+ - name: Format
+ run: crystal tool format --check
+
+ specs:
+ strategy:
+ fail-fast: false
+ matrix:
+ crystal_version:
+ - 1.6.2
+ experimental:
+ - false
+ runs-on: ubuntu-latest
+ env:
+ LUCKY_ENV: test
+ DB_HOST: localhost
+ continue-on-error: ${{ matrix.experimental }}
+ services:
+ postgres:
+ image: postgres:12-alpine
+ env:
+ POSTGRES_PASSWORD: postgres
+ ports:
+ - 5432:5432
+ # Set health checks to wait until postgres has started
+ options: >-
+ --health-cmd pg_isready
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Install Crystal
+ uses: crystal-lang/install-crystal@v1
+ with:
+ crystal: ${{ matrix.crystal_version }}
+
+ - name: Get yarn cache directory path
+ id: yarn-cache-dir-path
+ run: echo "::set-output name=dir::$(yarn cache dir)"
+
+ - name: Set up Yarn cache
+ uses: actions/cache@v2
+ with:
+ path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
+ key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-yarn-
+
+ - name: Set up Node cache
+ uses: actions/cache@v2
+ id: node-cache # use this to check for `cache-hit` (`steps.node-cache.outputs.cache-hit != 'true'`)
+ with:
+ path: '**/node_modules'
+ key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-node-
+ - name: Set up Crystal cache
+ uses: actions/cache@v2
+ id: crystal-cache
+ with:
+ path: |
+ ~/.cache/crystal
+ lib
+ lucky_tasks
+ key: ${{ runner.os }}-crystal-${{ hashFiles('**/shard.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-crystal-
+
+ - name: Install shards
+ if: steps.crystal-cache.outputs.cache-hit != 'true'
+ run: shards check || shards install
+
+ - name: Install yarn packages
+ if: steps.node-cache.outputs.cache-hit != 'true'
+ run: yarn install --frozen-lockfile --no-progress
+ - name: Compiling assets
+ run: yarn prod
+ - name: Install npm and Nexploit Repeater
+ run: |
+ sudo npm install -g @neuralegion/nexploit-cli --unsafe-perm=true
+ - name: Build lucky_tasks
+ if: steps.crystal-cache.outputs.cache-hit != 'true'
+ run: crystal build tasks.cr -o ./lucky_tasks
+
+ - name: Prepare database
+ run: |
+ ./lucky_tasks db.create
+ ./lucky_tasks db.migrate
+ ./lucky_tasks db.seed.required_data
+
+ - name: Run tests
+ run: crystal spec -Dwith_sec_tests
\ No newline at end of file
diff --git a/fixtures/src_template__sec_tester/expected/Procfile b/fixtures/src_template__sec_tester/expected/Procfile
new file mode 100644
index 00000000..e524d70d
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/Procfile
@@ -0,0 +1,2 @@
+web: bin/app
+release: lucky db.migrate
diff --git a/fixtures/src_template__sec_tester/expected/Procfile.dev b/fixtures/src_template__sec_tester/expected/Procfile.dev
new file mode 100644
index 00000000..d9a8173a
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/Procfile.dev
@@ -0,0 +1,3 @@
+system_check: script/system_check && sleep 100000
+web: lucky watch --reload-browser
+assets: yarn watch
diff --git a/fixtures/src_template__sec_tester/expected/README.md b/fixtures/src_template__sec_tester/expected/README.md
new file mode 100644
index 00000000..da2d97cc
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/README.md
@@ -0,0 +1,23 @@
+# test-project
+
+This is a project written using [Lucky](https://luckyframework.org). Enjoy!
+
+### Setting up the project
+
+1. [Install required dependencies](https://luckyframework.org/guides/getting-started/installing#install-required-dependencies)
+1. Update database settings in `config/database.cr`
+1. Run `script/setup`
+1. Run `lucky dev` to start the app
+
+### Using Docker for development
+
+1. [Install Docker](https://docs.docker.com/engine/install/)
+1. Run `docker compose up`
+
+The Docker container will boot all of the necessary components needed to run your Lucky application.
+To configure the container, update the `docker-compose.yml` file, and the `docker/development.dockerfile` file.
+
+
+### Learning Lucky
+
+Lucky uses the [Crystal](https://crystal-lang.org) programming language. You can learn about Lucky from the [Lucky Guides](https://luckyframework.org/guides/getting-started/why-lucky).
diff --git a/fixtures/src_template__sec_tester/expected/config/application.cr b/fixtures/src_template__sec_tester/expected/config/application.cr
new file mode 100644
index 00000000..c807149a
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/config/application.cr
@@ -0,0 +1,24 @@
+# This file may be used for custom Application configurations.
+# It will be loaded before other config files.
+#
+# Read more on configuration:
+# https://luckyframework.org/guides/getting-started/configuration#configuring-your-own-code
+
+# Use this code as an example:
+#
+# ```
+# module Application
+# Habitat.create do
+# setting support_email : String
+# setting lock_with_basic_auth : Bool
+# end
+# end
+#
+# Application.configure do |settings|
+# settings.support_email = "support@myapp.io"
+# settings.lock_with_basic_auth = LuckyEnv.staging?
+# end
+#
+# # In your application, call
+# # `Application.settings.support_email` anywhere you need it.
+# ```
diff --git a/fixtures/src_template__sec_tester/expected/config/colors.cr b/fixtures/src_template__sec_tester/expected/config/colors.cr
new file mode 100644
index 00000000..761ae940
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/config/colors.cr
@@ -0,0 +1,4 @@
+# This enables the color output when in development or test
+# Check out the Colorize docs for more information
+# https://crystal-lang.org/api/Colorize.html
+Colorize.enabled = LuckyEnv.development? || LuckyEnv.test?
diff --git a/fixtures/src_template__sec_tester/expected/config/cookies.cr b/fixtures/src_template__sec_tester/expected/config/cookies.cr
new file mode 100644
index 00000000..2d5055f2
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/config/cookies.cr
@@ -0,0 +1,25 @@
+require "./server"
+
+Lucky::Session.configure do |settings|
+ settings.key = "_test_project_session"
+end
+
+Lucky::CookieJar.configure do |settings|
+ settings.on_set = ->(cookie : HTTP::Cookie) {
+ # If ForceSSLHandler is enabled, only send cookies over HTTPS
+ cookie.secure(Lucky::ForceSSLHandler.settings.enabled)
+
+ # By default, don't allow reading cookies with JavaScript
+ cookie.http_only(true)
+
+ # Restrict cookies to a first-party or same-site context
+ cookie.samesite(:lax)
+
+ # Set all cookies to the root path by default
+ cookie.path("/")
+
+ # You can set other defaults for cookies here. For example:
+ #
+ # cookie.expires(1.year.from_now).domain("mydomain.com")
+ }
+end
diff --git a/fixtures/src_template__sec_tester/expected/config/database.cr b/fixtures/src_template__sec_tester/expected/config/database.cr
new file mode 100644
index 00000000..f614299a
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/config/database.cr
@@ -0,0 +1,29 @@
+database_name = "test_project_#{LuckyEnv.environment}"
+
+AppDatabase.configure do |settings|
+ if LuckyEnv.production?
+ settings.credentials = Avram::Credentials.parse(ENV["DATABASE_URL"])
+ else
+ settings.credentials = Avram::Credentials.parse?(ENV["DATABASE_URL"]?) || Avram::Credentials.new(
+ database: database_name,
+ hostname: ENV["DB_HOST"]? || "localhost",
+ port: ENV["DB_PORT"]?.try(&.to_i) || 5432,
+ # Some common usernames are "postgres", "root", or your system username (run 'whoami')
+ username: ENV["DB_USERNAME"]? || "postgres",
+ # Some Postgres installations require no password. Use "" if that is the case.
+ password: ENV["DB_PASSWORD"]? || "postgres"
+ )
+ end
+end
+
+Avram.configure do |settings|
+ settings.database_to_migrate = AppDatabase
+
+ # In production, allow lazy loading (N+1).
+ # In development and test, raise an error if you forget to preload associations
+ settings.lazy_load_enabled = LuckyEnv.production?
+
+ # Always parse `Time` values with these specific formats.
+ # Used for both database values, and datetime input fields.
+ # settings.time_formats << "%F"
+end
diff --git a/fixtures/src_template__sec_tester/expected/config/email.cr b/fixtures/src_template__sec_tester/expected/config/email.cr
new file mode 100644
index 00000000..7c875449
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/config/email.cr
@@ -0,0 +1,26 @@
+require "carbon_sendgrid_adapter"
+
+BaseEmail.configure do |settings|
+ if LuckyEnv.production?
+ # If you don't need to send emails, set the adapter to DevAdapter instead:
+ #
+ # settings.adapter = Carbon::DevAdapter.new
+ #
+ # If you do need emails, get a key from SendGrid and set an ENV variable
+ send_grid_key = send_grid_key_from_env
+ settings.adapter = Carbon::SendGridAdapter.new(api_key: send_grid_key)
+ elsif LuckyEnv.development?
+ settings.adapter = Carbon::DevAdapter.new(print_emails: true)
+ else
+ settings.adapter = Carbon::DevAdapter.new
+ end
+end
+
+private def send_grid_key_from_env
+ ENV["SEND_GRID_KEY"]? || raise_missing_key_message
+end
+
+private def raise_missing_key_message
+ puts "Missing SEND_GRID_KEY. Set the SEND_GRID_KEY env variable to 'unused' if not sending emails, or set the SEND_GRID_KEY ENV var.".colorize.red
+ exit(1)
+end
diff --git a/fixtures/src_template__sec_tester/expected/config/env.cr b/fixtures/src_template__sec_tester/expected/config/env.cr
new file mode 100644
index 00000000..3f364072
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/config/env.cr
@@ -0,0 +1,33 @@
+# Environments are managed using `LuckyEnv`. By default, development, production
+# and test are supported. See
+# https://luckyframework.org/guides/getting-started/configuration for details.
+#
+# The default environment is development unless the environment variable
+# LUCKY_ENV is set.
+#
+# Example:
+# ```
+# LuckyEnv.environment # => "development"
+# LuckyEnv.development? # => true
+# LuckyEnv.production? # => false
+# LuckyEnv.test? # => false
+# ```
+#
+# New environments can be added using the `LuckyEnv.add_env` macro.
+#
+# Example:
+# ```
+# LuckyEnv.add_env :staging
+# LuckyEnv.staging? # => false
+# ```
+#
+# To determine whether or not a `LuckyTask` is currently running, you can use
+# the `LuckyEnv.task?` predicate.
+#
+# Example:
+# ```
+# LuckyEnv.task? # => false
+# ```
+
+# Add a staging environment.
+# LuckyEnv.add_env :staging
diff --git a/fixtures/src_template__sec_tester/expected/config/error_handler.cr b/fixtures/src_template__sec_tester/expected/config/error_handler.cr
new file mode 100644
index 00000000..c6b736e3
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/config/error_handler.cr
@@ -0,0 +1,3 @@
+Lucky::ErrorHandler.configure do |settings|
+ settings.show_debug_output = !LuckyEnv.production?
+end
diff --git a/fixtures/src_template__sec_tester/expected/config/log.cr b/fixtures/src_template__sec_tester/expected/config/log.cr
new file mode 100644
index 00000000..0e8d7636
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/config/log.cr
@@ -0,0 +1,45 @@
+require "file_utils"
+
+if LuckyEnv.test?
+ # Logs to `tmp/test.log` so you can see what's happening without having
+ # a bunch of log output in your spec results.
+ FileUtils.mkdir_p("tmp")
+
+ backend = Log::IOBackend.new(File.new("tmp/test.log", mode: "w"))
+ backend.formatter = Lucky::PrettyLogFormatter.proc
+ Log.dexter.configure(:debug, backend)
+elsif LuckyEnv.production?
+ # Lucky uses JSON in production so logs can be searched more easily
+ #
+ # If you want logs like in develpoment use 'Lucky::PrettyLogFormatter.proc'.
+ backend = Log::IOBackend.new
+ backend.formatter = Dexter::JSONLogFormatter.proc
+ Log.dexter.configure(:info, backend)
+else
+ # Use a pretty formatter printing to STDOUT in development
+ backend = Log::IOBackend.new
+ backend.formatter = Lucky::PrettyLogFormatter.proc
+ Log.dexter.configure(:debug, backend)
+ DB::Log.level = :info
+end
+
+# Lucky only logs when before/after pipes halt by redirecting, or rendering a
+# response. Pipes that run without halting are not logged.
+#
+# If you want to log every pipe that runs, set the log level to ':info'
+Lucky::ContinuedPipeLog.dexter.configure(:none)
+
+# Lucky only logs failed queries by default.
+#
+# Set the log to ':info' to log all queries
+Avram::QueryLog.dexter.configure(:none)
+
+# Skip logging static assets requests in development
+Lucky::LogHandler.configure do |settings|
+ if LuckyEnv.development?
+ settings.skip_if = ->(context : HTTP::Server::Context) {
+ context.request.method.downcase == "get" &&
+ context.request.resource.starts_with?(/\/css\/|\/js\/|\/assets\/|\/favicon\.ico/)
+ }
+ end
+end
diff --git a/fixtures/src_template__sec_tester/expected/config/route_helper.cr b/fixtures/src_template__sec_tester/expected/config/route_helper.cr
new file mode 100644
index 00000000..ede1f328
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/config/route_helper.cr
@@ -0,0 +1,10 @@
+# This is used when generating URLs for your application
+Lucky::RouteHelper.configure do |settings|
+ if LuckyEnv.production?
+ # Example: https://my_app.com
+ settings.base_uri = ENV.fetch("APP_DOMAIN")
+ else
+ # Set domain to the default host/port in development/test
+ settings.base_uri = "http://localhost:#{Lucky::ServerSettings.port}"
+ end
+end
diff --git a/fixtures/src_template__sec_tester/expected/config/server.cr b/fixtures/src_template__sec_tester/expected/config/server.cr
new file mode 100644
index 00000000..c72314be
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/config/server.cr
@@ -0,0 +1,65 @@
+# Here is where you configure the Lucky server
+#
+# Look at config/route_helper.cr if you want to change the domain used when
+# generating links with `Action.url`.
+Lucky::Server.configure do |settings|
+ if LuckyEnv.production?
+ settings.secret_key_base = secret_key_from_env
+ settings.host = "0.0.0.0"
+ settings.port = ENV["PORT"].to_i
+ settings.gzip_enabled = true
+ # By default certain content types will be gzipped.
+ # For a full list look in
+ # https://github.com/luckyframework/lucky/blob/main/src/lucky/server.cr
+ # To add additional extensions do something like this:
+ # settings.gzip_content_types << "content/type"
+ else
+ settings.secret_key_base = "1234567890"
+ # Change host/port in config/watch.yml
+ # Alternatively, you can set the DEV_PORT env to set the port for local development
+ settings.host = Lucky::ServerSettings.host
+ settings.port = Lucky::ServerSettings.port
+ end
+
+ # By default Lucky will serve static assets in development and production.
+ #
+ # However you could use a CDN when in production like this:
+ #
+ # Lucky::Server.configure do |settings|
+ # if LuckyEnv.production?
+ # settings.asset_host = "https://mycdnhost.com"
+ # else
+ # settings.asset_host = ""
+ # end
+ # end
+ settings.asset_host = "" # Lucky will serve assets
+end
+
+Lucky::ForceSSLHandler.configure do |settings|
+ # To force SSL in production, uncomment the lines below.
+ # This will cause http requests to be redirected to https:
+ #
+ # settings.enabled = LuckyEnv.production?
+ # settings.strict_transport_security = {max_age: 1.year, include_subdomains: true}
+ #
+ # Or, leave it disabled:
+ settings.enabled = false
+end
+
+# Set a unique ID for each HTTP request.
+# To enable the request ID, uncomment the lines below.
+# You can set your own custom String, or use a random UUID.
+# Lucky::RequestIdHandler.configure do |settings|
+# settings.set_request_id = ->(context : HTTP::Server::Context) {
+# UUID.random.to_s
+# }
+# end
+
+private def secret_key_from_env
+ ENV["SECRET_KEY_BASE"]? || raise_missing_secret_key_in_production
+end
+
+private def raise_missing_secret_key_in_production
+ puts "Please set the SECRET_KEY_BASE environment variable. You can generate a secret key with 'lucky gen.secret_key'".colorize.red
+ exit(1)
+end
diff --git a/fixtures/src_template__sec_tester/expected/config/watch.yml b/fixtures/src_template__sec_tester/expected/config/watch.yml
new file mode 100644
index 00000000..3a59b410
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/config/watch.yml
@@ -0,0 +1,3 @@
+host: 127.0.0.1
+port: 3000
+reload_port: 3001
diff --git a/fixtures/src_template__sec_tester/expected/db/migrations/.keep b/fixtures/src_template__sec_tester/expected/db/migrations/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__sec_tester/expected/docker-compose.yml b/fixtures/src_template__sec_tester/expected/docker-compose.yml
new file mode 100644
index 00000000..d00779db
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/docker-compose.yml
@@ -0,0 +1,45 @@
+version: "3.8"
+services:
+ lucky:
+ build:
+ context: .
+ dockerfile: docker/development.dockerfile
+ environment:
+ DATABASE_URL: postgres://lucky:password@postgres:5432/lucky
+ DEV_HOST: "0.0.0.0"
+ volumes:
+ - .:/app
+ - node_modules:/app/node_modules
+ - shards_lib:/app/lib
+ - app_bin:/app/bin
+ - build_cache:/root/.cache
+ depends_on:
+ - postgres
+ ports:
+ - 3000:3000 # This is the Lucky Server port
+ - 3001:3001 # This is the Lucky watcher reload port
+
+ entrypoint: ["docker/dev_entrypoint.sh"]
+
+ postgres:
+ image: postgres:14-alpine
+ environment:
+ POSTGRES_USER: lucky
+ POSTGRES_PASSWORD: password
+ POSTGRES_DB: lucky
+ volumes:
+ - postgres_data:/var/lib/postgresql/data
+ ports:
+ # The postgres database container is exposed on the host at port 6543 to
+ # allow connecting directly to it with postgres clients. The port differs
+ # from the postgres default to avoid conflict with existing postgres
+ # servers. Connect to a running postgres container with:
+ # postgres://lucky:password@localhost:6543/lucky
+ - 6543:5432
+
+volumes:
+ postgres_data:
+ node_modules:
+ shards_lib:
+ app_bin:
+ build_cache:
diff --git a/fixtures/src_template__sec_tester/expected/docker/dev_entrypoint.sh b/fixtures/src_template__sec_tester/expected/docker/dev_entrypoint.sh
new file mode 100755
index 00000000..d16ef6ab
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/docker/dev_entrypoint.sh
@@ -0,0 +1,46 @@
+#!/bin/bash
+
+set -euo pipefail
+
+# This is the entrypoint script used for development docker workflows.
+# By default it will:
+# - Install dependencies.
+# - Run migrations.
+# - Start the dev server.
+# It also accepts any commands to be run instead.
+
+
+warnfail () {
+ echo "$@" >&2
+ exit 1
+}
+
+case ${1:-} in
+ "") # If no arguments are provided, start lucky dev server.
+ ;;
+
+ *) # If any arguments are provided, execute them instead.
+ exec "$@"
+esac
+
+if ! [ -d bin ] ; then
+ echo 'Creating bin directory'
+ mkdir bin
+fi
+echo 'Installing npm packages...'
+yarn install
+if ! shards check ; then
+ echo 'Installing shards...'
+ shards install
+fi
+
+echo 'Waiting for postgres to be available...'
+./docker/wait-for-it.sh -q postgres:5432
+
+if ! psql -d "$DATABASE_URL" -c '\d migrations' > /dev/null ; then
+ echo 'Finishing database setup...'
+ lucky db.migrate
+fi
+
+echo 'Starting lucky dev server...'
+exec lucky dev
diff --git a/fixtures/src_template__sec_tester/expected/docker/development.dockerfile b/fixtures/src_template__sec_tester/expected/docker/development.dockerfile
new file mode 100644
index 00000000..be3dba5f
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/docker/development.dockerfile
@@ -0,0 +1,34 @@
+FROM crystallang/crystal:1.6.2
+
+# Install utilities required to make this Dockerfile run
+RUN apt-get update && \
+ apt-get install -y wget
+# Add the nodesource ppa to apt. Update this to change the nodejs version.
+RUN wget https://deb.nodesource.com/setup_16.x -O- | bash
+
+# Apt installs:
+# - nodejs (from above ppa) is required for front-end apps.
+# - Postgres cli tools are required for lucky-cli.
+# - tmux is required for the Overmind process manager.
+RUN apt-get update && \
+ apt-get install -y nodejs postgresql-client tmux && \
+ rm -rf /var/lib/apt/lists/*
+
+# NPM global installs:
+# - Yarn is the default package manager for the node component of a lucky
+# browser app.
+# - Mix is the default asset compiler.
+RUN npm install -g yarn mix
+
+# Install lucky cli
+WORKDIR /lucky/cli
+RUN git clone https://github.com/luckyframework/lucky_cli . && \
+ git checkout v1.0.0 && \
+ shards build --without-development && \
+ cp bin/lucky /usr/bin
+
+WORKDIR /app
+ENV DATABASE_URL=postgres://postgres:postgres@host.docker.internal:5432/postgres
+EXPOSE 3000
+EXPOSE 3001
+
diff --git a/fixtures/src_template__sec_tester/expected/docker/wait-for-it.sh b/fixtures/src_template__sec_tester/expected/docker/wait-for-it.sh
new file mode 100755
index 00000000..06e0638c
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/docker/wait-for-it.sh
@@ -0,0 +1,189 @@
+#!/usr/bin/bash
+#
+# Pulled from https://github.com/vishnubob/wait-for-it on 2022-02-28.
+# Licensed under the MIT license as of 81b1373f.
+#
+# Below this line, wait-for-it is the original work of the author.
+#
+# Use this script to test if a given TCP host/port are available
+
+WAITFORIT_cmdname=${0##*/}
+
+echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }
+
+usage()
+{
+ cat << USAGE >&2
+Usage:
+ $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args]
+ -h HOST | --host=HOST Host or IP under test
+ -p PORT | --port=PORT TCP port under test
+ Alternatively, you specify the host and port as host:port
+ -s | --strict Only execute subcommand if the test succeeds
+ -q | --quiet Don't output any status messages
+ -t TIMEOUT | --timeout=TIMEOUT
+ Timeout in seconds, zero for no timeout
+ -- COMMAND ARGS Execute command with args after the test finishes
+USAGE
+ exit 1
+}
+
+wait_for()
+{
+ if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
+ echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
+ else
+ echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout"
+ fi
+ WAITFORIT_start_ts=$(date +%s)
+ while :
+ do
+ if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then
+ nc -z $WAITFORIT_HOST $WAITFORIT_PORT
+ WAITFORIT_result=$?
+ else
+ (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1
+ WAITFORIT_result=$?
+ fi
+ if [[ $WAITFORIT_result -eq 0 ]]; then
+ WAITFORIT_end_ts=$(date +%s)
+ echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds"
+ break
+ fi
+ sleep 1
+ done
+ return $WAITFORIT_result
+}
+
+wait_for_wrapper()
+{
+ # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
+ if [[ $WAITFORIT_QUIET -eq 1 ]]; then
+ timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
+ else
+ timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
+ fi
+ WAITFORIT_PID=$!
+ trap "kill -INT -$WAITFORIT_PID" INT
+ wait $WAITFORIT_PID
+ WAITFORIT_RESULT=$?
+ if [[ $WAITFORIT_RESULT -ne 0 ]]; then
+ echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
+ fi
+ return $WAITFORIT_RESULT
+}
+
+# process arguments
+while [[ $# -gt 0 ]]
+do
+ case "$1" in
+ *:* )
+ WAITFORIT_hostport=(${1//:/ })
+ WAITFORIT_HOST=${WAITFORIT_hostport[0]}
+ WAITFORIT_PORT=${WAITFORIT_hostport[1]}
+ shift 1
+ ;;
+ --child)
+ WAITFORIT_CHILD=1
+ shift 1
+ ;;
+ -q | --quiet)
+ WAITFORIT_QUIET=1
+ shift 1
+ ;;
+ -s | --strict)
+ WAITFORIT_STRICT=1
+ shift 1
+ ;;
+ -h)
+ WAITFORIT_HOST="$2"
+ if [[ $WAITFORIT_HOST == "" ]]; then break; fi
+ shift 2
+ ;;
+ --host=*)
+ WAITFORIT_HOST="${1#*=}"
+ shift 1
+ ;;
+ -p)
+ WAITFORIT_PORT="$2"
+ if [[ $WAITFORIT_PORT == "" ]]; then break; fi
+ shift 2
+ ;;
+ --port=*)
+ WAITFORIT_PORT="${1#*=}"
+ shift 1
+ ;;
+ -t)
+ WAITFORIT_TIMEOUT="$2"
+ if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi
+ shift 2
+ ;;
+ --timeout=*)
+ WAITFORIT_TIMEOUT="${1#*=}"
+ shift 1
+ ;;
+ --)
+ shift
+ WAITFORIT_CLI=("$@")
+ break
+ ;;
+ --help)
+ usage
+ ;;
+ *)
+ echoerr "Unknown argument: $1"
+ usage
+ ;;
+ esac
+done
+
+if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then
+ echoerr "Error: you need to provide a host and port to test."
+ usage
+fi
+
+WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15}
+WAITFORIT_STRICT=${WAITFORIT_STRICT:-0}
+WAITFORIT_CHILD=${WAITFORIT_CHILD:-0}
+WAITFORIT_QUIET=${WAITFORIT_QUIET:-0}
+
+# Check to see if timeout is from busybox?
+WAITFORIT_TIMEOUT_PATH=$(type -p timeout)
+WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH)
+
+WAITFORIT_BUSYTIMEFLAG=""
+if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then
+ WAITFORIT_ISBUSY=1
+ # Check if busybox timeout uses -t flag
+ # (recent Alpine versions don't support -t anymore)
+ if timeout &>/dev/stdout | grep -q -e '-t '; then
+ WAITFORIT_BUSYTIMEFLAG="-t"
+ fi
+else
+ WAITFORIT_ISBUSY=0
+fi
+
+if [[ $WAITFORIT_CHILD -gt 0 ]]; then
+ wait_for
+ WAITFORIT_RESULT=$?
+ exit $WAITFORIT_RESULT
+else
+ if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
+ wait_for_wrapper
+ WAITFORIT_RESULT=$?
+ else
+ wait_for
+ WAITFORIT_RESULT=$?
+ fi
+fi
+
+if [[ $WAITFORIT_CLI != "" ]]; then
+ if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then
+ echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess"
+ exit $WAITFORIT_RESULT
+ fi
+ exec "${WAITFORIT_CLI[@]}"
+else
+ exit $WAITFORIT_RESULT
+fi
+
diff --git a/fixtures/src_template__sec_tester/expected/script/helpers/function_helpers b/fixtures/src_template__sec_tester/expected/script/helpers/function_helpers
new file mode 100644
index 00000000..388fa67e
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/script/helpers/function_helpers
@@ -0,0 +1,67 @@
+#!/usr/bin/env bash
+
+# This file contains a set of functions used as helpers
+# for various tasks. Read the examples for each one for
+# more information. Feel free to put any additional helper
+# functions you may need for your app
+
+
+# Returns true if the command $1 is not found
+# example:
+# if command_not_found "yarn"; then
+# echo "no yarn"
+# fi
+command_not_found() {
+ ! command -v $1 > /dev/null
+ return $?
+}
+
+# Returns true if the command $1 is not running
+# You must supply the full command to check as an argument
+# example:
+# if command_not_running "redis-cli ping"; then
+# print_error "Redis is not running"
+# fi
+command_not_running() {
+ $1
+ if [ $? -ne 0 ]; then
+ true
+ else
+ false
+ fi
+}
+
+# Returns true if the OS is macOS
+# example:
+# if is_mac; then
+# echo "do mac stuff"
+# fi
+is_mac() {
+ if [[ "$OSTYPE" == "darwin"* ]]; then
+ true
+ else
+ false
+ fi
+}
+
+# Returns true if the OS is linux based
+# example:
+# if is_linux; then
+# echo "do linux stuff"
+# fi
+is_linux() {
+ if [[ "$OSTYPE" == "linux"* ]]; then
+ true
+ else
+ false
+ fi
+}
+
+# Prints error and exit.
+# example:
+# print_error "Redis is not running. Run it with some_command"
+print_error() {
+ printf "${BOLD_RED_COLOR}There is a problem with your system setup:\n\n"
+ printf "${BOLD_RED_COLOR}$1 \n\n" | indent
+ exit 1
+}
diff --git a/fixtures/src_template__sec_tester/expected/script/helpers/text_helpers b/fixtures/src_template__sec_tester/expected/script/helpers/text_helpers
new file mode 100644
index 00000000..34b77a8c
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/script/helpers/text_helpers
@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+
+# This file contains a set of functions used to format text,
+# and make printing text a little easier. Feel free to put
+# any additional functions you need for formatting your shell
+# output text.
+
+# Colors
+BOLD_RED_COLOR="\e[1m\e[31m"
+
+# Indents the text 2 spaces
+# example:
+# printf "Hello" | indent
+indent() {
+ while read LINE; do
+ echo " $LINE" || true
+ done
+}
+
+# Prints out an arrow to your custom notice
+# example:
+# notice "Installing new magic"
+notice() {
+ printf "\n▸ $1\n"
+}
+
+# Prints out a check mark and Done.
+# example:
+# print_done
+print_done() {
+ printf "✔ Done\n" | indent
+}
diff --git a/fixtures/src_template__sec_tester/expected/script/setup b/fixtures/src_template__sec_tester/expected/script/setup
new file mode 100755
index 00000000..bf0bc317
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/script/setup
@@ -0,0 +1,45 @@
+#!/usr/bin/env bash
+
+# Exit if any subcommand fails
+set -e
+set -o pipefail
+
+source script/helpers/text_helpers
+
+
+notice "Running System Check"
+./script/system_check
+print_done
+
+notice "Installing node dependencies"
+yarn install --no-progress | indent
+
+notice "Compiling assets"
+yarn dev | indent
+
+print_done
+
+notice "Installing shards"
+shards install --ignore-crystal-version | indent
+
+if [ ! -f ".env" ]; then
+ notice "No .env found. Creating one."
+ touch .env
+ print_done
+fi
+
+notice "Creating the database"
+lucky db.create | indent
+
+notice "Verifying postgres connection"
+lucky db.verify_connection | indent
+
+notice "Migrating the database"
+lucky db.migrate | indent
+
+notice "Seeding the database with required and sample records"
+lucky db.seed.required_data | indent
+lucky db.seed.sample_data | indent
+
+print_done
+notice "Run 'lucky dev' to start the app"
diff --git a/fixtures/src_template__sec_tester/expected/script/system_check b/fixtures/src_template__sec_tester/expected/script/system_check
new file mode 100755
index 00000000..c27c926b
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/script/system_check
@@ -0,0 +1,37 @@
+#!/usr/bin/env bash
+
+source script/helpers/text_helpers
+source script/helpers/function_helpers
+
+# Use this script to check the system for required tools and process that your app needs.
+# A few helper functions are provided to make writing bash a little easier. See the
+# script/helpers/function_helpers file for more examples.
+#
+# A few examples you might use here:
+# * 'lucky db.verify_connection' to test postgres can be connected
+# * Checking that elasticsearch, redis, or postgres is installed and/or booted
+# * Note: Booting additional processes for things like mail, background jobs, etc...
+# should go in your Procfile.dev.
+
+if command_not_found "yarn"; then
+ print_error "Yarn is not installed\n See https://yarnpkg.com/lang/en/docs/install/ for install instructions."
+fi
+
+if command_not_found "createdb"; then
+ MSG="Please install the postgres CLI tools, then try again."
+ if is_mac; then
+ MSG="$MSG\nIf you're using Postgres.app, see https://postgresapp.com/documentation/cli-tools.html."
+ fi
+ MSG="$MSG\nSee https://www.postgresql.org/docs/current/tutorial-install.html for install instructions."
+
+ print_error "$MSG"
+fi
+
+
+## CUSTOM PRE-BOOT CHECKS ##
+# example:
+# if command_not_running "redis-cli ping"; then
+# print_error "Redis is not running."
+# fi
+
+
diff --git a/fixtures/src_template__sec_tester/expected/spec/setup/clean_database.cr b/fixtures/src_template__sec_tester/expected/spec/setup/clean_database.cr
new file mode 100644
index 00000000..a1bc631c
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/spec/setup/clean_database.cr
@@ -0,0 +1,3 @@
+Spec.before_each do
+ AppDatabase.truncate
+end
diff --git a/fixtures/src_template__sec_tester/expected/spec/setup/reset_emails.cr b/fixtures/src_template__sec_tester/expected/spec/setup/reset_emails.cr
new file mode 100644
index 00000000..140ab416
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/spec/setup/reset_emails.cr
@@ -0,0 +1,3 @@
+Spec.before_each do
+ Carbon::DevAdapter.reset
+end
diff --git a/fixtures/src_template__sec_tester/expected/spec/setup/setup_database.cr b/fixtures/src_template__sec_tester/expected/spec/setup/setup_database.cr
new file mode 100644
index 00000000..393c6da3
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/spec/setup/setup_database.cr
@@ -0,0 +1,2 @@
+Db::Create.new(quiet: true).call
+Db::Migrate.new(quiet: true).call
diff --git a/fixtures/src_template__sec_tester/expected/spec/setup/start_app_server.cr b/fixtures/src_template__sec_tester/expected/spec/setup/start_app_server.cr
new file mode 100644
index 00000000..ff0bfeeb
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/spec/setup/start_app_server.cr
@@ -0,0 +1,10 @@
+app_server = AppServer.new
+
+spawn do
+ app_server.listen
+end
+
+Spec.after_suite do
+ LuckyFlow.shutdown
+ app_server.close
+end
diff --git a/fixtures/src_template__sec_tester/expected/spec/spec_helper.cr b/fixtures/src_template__sec_tester/expected/spec/spec_helper.cr
new file mode 100644
index 00000000..187f4753
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/spec/spec_helper.cr
@@ -0,0 +1,24 @@
+ENV["LUCKY_ENV"] = "test"
+ENV["DEV_PORT"] = "5001"
+require "spec"
+require "lucky_flow"
+require "lucky_flow/ext/lucky"
+require "lucky_flow/ext/avram"
+require "../src/app"
+require "./support/flows/base_flow"
+require "./support/**"
+require "../db/migrations/**"
+
+# Add/modify files in spec/setup to start/configure programs or run hooks
+#
+# By default there are scripts for setting up and cleaning the database,
+# configuring LuckyFlow, starting the app server, etc.
+require "./setup/**"
+
+include Carbon::Expectations
+include Lucky::RequestExpectations
+include LuckyFlow::Expectations
+
+Avram::Migrator::Runner.new.ensure_migrated!
+Avram::SchemaEnforcer.ensure_correct_column_mappings!
+Habitat.raise_if_missing_settings!
diff --git a/fixtures/src_template__sec_tester/expected/spec/support/.keep b/fixtures/src_template__sec_tester/expected/spec/support/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__sec_tester/expected/spec/support/api_client.cr b/fixtures/src_template__sec_tester/expected/spec/support/api_client.cr
new file mode 100644
index 00000000..ef251251
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/spec/support/api_client.cr
@@ -0,0 +1,8 @@
+class ApiClient < Lucky::BaseHTTPClient
+ app AppServer.new
+
+ def initialize
+ super
+ headers("Content-Type": "application/json")
+ end
+end
diff --git a/fixtures/src_template__sec_tester/expected/spec/support/factories/.keep b/fixtures/src_template__sec_tester/expected/spec/support/factories/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__sec_tester/expected/src/actions/api_action.cr b/fixtures/src_template__sec_tester/expected/src/actions/api_action.cr
new file mode 100644
index 00000000..fac02c8b
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/src/actions/api_action.cr
@@ -0,0 +1,11 @@
+# Include modules and add methods that are for all API requests
+abstract class ApiAction < Lucky::Action
+ # APIs typically do not need to send cookie/session data.
+ # Remove this line if you want to send cookies in the response header.
+ disable_cookies
+ accepted_formats [:json]
+
+ # By default all actions are required to use underscores to separate words.
+ # Add 'include Lucky::SkipRouteStyleCheck' to your actions if you wish to ignore this check for specific routes.
+ include Lucky::EnforceUnderscoredRoute
+end
diff --git a/fixtures/src_template__sec_tester/expected/src/actions/errors/show.cr b/fixtures/src_template__sec_tester/expected/src/actions/errors/show.cr
new file mode 100644
index 00000000..d01ed541
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/src/actions/errors/show.cr
@@ -0,0 +1,63 @@
+# This class handles error responses and reporting.
+#
+# https://luckyframework.org/guides/http-and-routing/error-handling
+class Errors::Show < Lucky::ErrorAction
+ DEFAULT_MESSAGE = "Something went wrong."
+ default_format :html
+ dont_report [Lucky::RouteNotFoundError, Avram::RecordNotFoundError]
+
+ def render(error : Lucky::RouteNotFoundError | Avram::RecordNotFoundError)
+ if html?
+ error_html "Sorry, we couldn't find that page.", status: 404
+ else
+ error_json "Not found", status: 404
+ end
+ end
+
+ # When the request is JSON and an InvalidOperationError is raised, show a
+ # helpful error with the param that is invalid, and what was wrong with it.
+ def render(error : Avram::InvalidOperationError)
+ if html?
+ error_html DEFAULT_MESSAGE, status: 500
+ else
+ error_json \
+ message: error.renderable_message,
+ details: error.renderable_details,
+ param: error.invalid_attribute_name,
+ status: 400
+ end
+ end
+
+ # Always keep this below other 'render' methods or it may override your
+ # custom 'render' methods.
+ def render(error : Lucky::RenderableError)
+ if html?
+ error_html DEFAULT_MESSAGE, status: error.renderable_status
+ else
+ error_json error.renderable_message, status: error.renderable_status
+ end
+ end
+
+ # If none of the 'render' methods return a response for the raised Exception,
+ # Lucky will use this method.
+ def default_render(error : Exception) : Lucky::Response
+ if html?
+ error_html DEFAULT_MESSAGE, status: 500
+ else
+ error_json DEFAULT_MESSAGE, status: 500
+ end
+ end
+
+ private def error_html(message : String, status : Int)
+ context.response.status_code = status
+ html_with_status Errors::ShowPage, status, message: message, status_code: status
+ end
+
+ private def error_json(message : String, status : Int, details = nil, param = nil)
+ json ErrorSerializer.new(message: message, details: details, param: param), status: status
+ end
+
+ private def report(error : Exception) : Nil
+ # Send to Rollbar, send an email, etc.
+ end
+end
diff --git a/fixtures/src_template__sec_tester/expected/src/actions/home/index.cr b/fixtures/src_template__sec_tester/expected/src/actions/home/index.cr
new file mode 100644
index 00000000..2d39a100
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/src/actions/home/index.cr
@@ -0,0 +1,5 @@
+class Home::Index < BrowserAction
+ get "/" do
+ html Lucky::WelcomePage
+ end
+end
diff --git a/fixtures/src_template__sec_tester/expected/src/actions/mixins/.keep b/fixtures/src_template__sec_tester/expected/src/actions/mixins/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__sec_tester/expected/src/app.cr b/fixtures/src_template__sec_tester/expected/src/app.cr
new file mode 100644
index 00000000..9d3ee3c7
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/src/app.cr
@@ -0,0 +1,26 @@
+require "./shards"
+
+# Load the asset manifest
+Lucky::AssetHelpers.load_manifest "public/mix-manifest.json"
+
+require "../config/server"
+require "./app_database"
+require "../config/**"
+require "./models/base_model"
+require "./models/mixins/**"
+require "./models/**"
+require "./queries/mixins/**"
+require "./queries/**"
+require "./operations/mixins/**"
+require "./operations/**"
+require "./serializers/base_serializer"
+require "./serializers/**"
+require "./emails/base_email"
+require "./emails/**"
+require "./actions/mixins/**"
+require "./actions/**"
+require "./components/base_component"
+require "./components/**"
+require "./pages/**"
+require "../db/migrations/**"
+require "./app_server"
diff --git a/fixtures/src_template__sec_tester/expected/src/app_database.cr b/fixtures/src_template__sec_tester/expected/src/app_database.cr
new file mode 100644
index 00000000..0efd4f50
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/src/app_database.cr
@@ -0,0 +1,2 @@
+class AppDatabase < Avram::Database
+end
diff --git a/fixtures/src_template__sec_tester/expected/src/app_server.cr b/fixtures/src_template__sec_tester/expected/src/app_server.cr
new file mode 100644
index 00000000..8ec16c3c
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/src/app_server.cr
@@ -0,0 +1,26 @@
+class AppServer < Lucky::BaseAppServer
+ # Learn about middleware with HTTP::Handlers:
+ # https://luckyframework.org/guides/http-and-routing/http-handlers
+ def middleware : Array(HTTP::Handler)
+ [
+ Lucky::RequestIdHandler.new,
+ Lucky::ForceSSLHandler.new,
+ Lucky::HttpMethodOverrideHandler.new,
+ Lucky::LogHandler.new,
+ Lucky::ErrorHandler.new(action: Errors::Show),
+ Lucky::RemoteIpHandler.new,
+ Lucky::RouteHandler.new,
+ Lucky::StaticCompressionHandler.new("./public", file_ext: "gz", content_encoding: "gzip"),
+ Lucky::StaticFileHandler.new("./public", fallthrough: false, directory_listing: false),
+ Lucky::RouteNotFoundHandler.new,
+ ] of HTTP::Handler
+ end
+
+ def protocol
+ "http"
+ end
+
+ def listen
+ server.listen(host, port, reuse_port: false)
+ end
+end
diff --git a/fixtures/src_template__sec_tester/expected/src/emails/base_email.cr b/fixtures/src_template__sec_tester/expected/src/emails/base_email.cr
new file mode 100644
index 00000000..656f4f11
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/src/emails/base_email.cr
@@ -0,0 +1,15 @@
+# Learn about sending emails
+# https://luckyframework.org/guides/emails/sending-emails-with-carbon
+abstract class BaseEmail < Carbon::Email
+ # You can add defaults using the 'inherited' hook
+ #
+ # Example:
+ #
+ # macro inherited
+ # from default_from
+ # end
+ #
+ # def default_from
+ # Carbon::Address.new("support@app.com")
+ # end
+end
diff --git a/fixtures/src_template__sec_tester/expected/src/models/base_model.cr b/fixtures/src_template__sec_tester/expected/src/models/base_model.cr
new file mode 100644
index 00000000..6bafeb84
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/src/models/base_model.cr
@@ -0,0 +1,5 @@
+abstract class BaseModel < Avram::Model
+ def self.database : Avram::Database.class
+ AppDatabase
+ end
+end
diff --git a/fixtures/src_template__sec_tester/expected/src/models/mixins/.keep b/fixtures/src_template__sec_tester/expected/src/models/mixins/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__sec_tester/expected/src/operations/.keep b/fixtures/src_template__sec_tester/expected/src/operations/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__sec_tester/expected/src/operations/mixins/.keep b/fixtures/src_template__sec_tester/expected/src/operations/mixins/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__sec_tester/expected/src/queries/.keep b/fixtures/src_template__sec_tester/expected/src/queries/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__sec_tester/expected/src/queries/mixins/.keep b/fixtures/src_template__sec_tester/expected/src/queries/mixins/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__sec_tester/expected/src/serializers/.keep b/fixtures/src_template__sec_tester/expected/src/serializers/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__sec_tester/expected/src/serializers/base_serializer.cr b/fixtures/src_template__sec_tester/expected/src/serializers/base_serializer.cr
new file mode 100644
index 00000000..3ad0a669
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/src/serializers/base_serializer.cr
@@ -0,0 +1,7 @@
+abstract class BaseSerializer < Lucky::Serializer
+ def self.for_collection(collection : Enumerable, *args, **named_args)
+ collection.map do |object|
+ new(object, *args, **named_args)
+ end
+ end
+end
diff --git a/fixtures/src_template__sec_tester/expected/src/serializers/error_serializer.cr b/fixtures/src_template__sec_tester/expected/src/serializers/error_serializer.cr
new file mode 100644
index 00000000..21a53aa2
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/src/serializers/error_serializer.cr
@@ -0,0 +1,14 @@
+# This is the default error serializer generated by Lucky.
+# Feel free to customize it in any way you like.
+class ErrorSerializer < BaseSerializer
+ def initialize(
+ @message : String,
+ @details : String? = nil,
+ @param : String? = nil # so you can track which param (if any) caused the problem
+ )
+ end
+
+ def render
+ {message: @message, param: @param, details: @details}
+ end
+end
diff --git a/fixtures/src_template__sec_tester/expected/src/shards.cr b/fixtures/src_template__sec_tester/expected/src/shards.cr
new file mode 100644
index 00000000..1afc72cb
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/src/shards.cr
@@ -0,0 +1,8 @@
+# Load .env file before any other config or app code
+require "lucky_env"
+LuckyEnv.load?(".env")
+
+# Require your shards here
+require "lucky"
+require "avram/lucky"
+require "carbon"
diff --git a/fixtures/src_template__sec_tester/expected/src/start_server.cr b/fixtures/src_template__sec_tester/expected/src/start_server.cr
new file mode 100644
index 00000000..9df5d1fd
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/src/start_server.cr
@@ -0,0 +1,16 @@
+require "./app"
+
+Habitat.raise_if_missing_settings!
+
+if LuckyEnv.development?
+ Avram::Migrator::Runner.new.ensure_migrated!
+ Avram::SchemaEnforcer.ensure_correct_column_mappings!
+end
+
+app_server = AppServer.new
+
+Signal::INT.trap do
+ app_server.close
+end
+
+app_server.listen
diff --git a/fixtures/src_template__sec_tester/expected/src/test_project.cr b/fixtures/src_template__sec_tester/expected/src/test_project.cr
new file mode 100644
index 00000000..68e1a8d2
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/src/test_project.cr
@@ -0,0 +1,6 @@
+# Typically you will not use or modify this file. 'shards build' and some
+# other crystal tools will sometimes use this.
+#
+# When this file is compiled/run it will require and run 'start_server',
+# which as its name implies will start the server for you app.
+require "./start_server"
diff --git a/fixtures/src_template__sec_tester/expected/tasks.cr b/fixtures/src_template__sec_tester/expected/tasks.cr
new file mode 100644
index 00000000..5a892d4d
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/tasks.cr
@@ -0,0 +1,25 @@
+# This file loads your app and all your tasks when running 'lucky'
+#
+# Run 'lucky --help' to see all available tasks.
+#
+# Learn to create your own tasks:
+# https://luckyframework.org/guides/command-line-tasks/custom-tasks
+
+# See `LuckyEnv#task?`
+ENV["LUCKY_TASK"] = "true"
+
+# Load Lucky and the app (actions, models, etc.)
+require "./src/app"
+require "lucky_task"
+
+# You can add your own tasks here in the ./tasks folder
+require "./tasks/**"
+
+# Load migrations
+require "./db/migrations/**"
+
+# Load Lucky tasks (dev, routes, etc.)
+require "lucky/tasks/**"
+require "avram/lucky/tasks"
+
+LuckyTask::Runner.run
diff --git a/fixtures/src_template__sec_tester/expected/tasks/.keep b/fixtures/src_template__sec_tester/expected/tasks/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/fixtures/src_template__sec_tester/expected/tasks/db/seed/required_data.cr b/fixtures/src_template__sec_tester/expected/tasks/db/seed/required_data.cr
new file mode 100644
index 00000000..d866040f
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/tasks/db/seed/required_data.cr
@@ -0,0 +1,30 @@
+require "../../../spec/support/factories/**"
+
+# Add seeds here that are *required* for your app to work.
+# For example, you might need at least one admin user or you might need at least
+# one category for your blog posts for the app to work.
+#
+# Use `Db::Seed::SampleData` if your only want to add sample data helpful for
+# development.
+class Db::Seed::RequiredData < LuckyTask::Task
+ summary "Add database records required for the app to work"
+
+ def call
+ # Using a Avram::Factory:
+ #
+ # Use the defaults, but override just the email
+ # UserFactory.create &.email("me@example.com")
+
+ # Using a SaveOperation:
+ #
+ # SaveUser.create!(email: "me@example.com", name: "Jane")
+ #
+ # You likely want to be able to run this file more than once. To do that,
+ # only create the record if it doesn't exist yet:
+ #
+ # unless UserQuery.new.email("me@example.com").first?
+ # SaveUser.create!(email: "me@example.com", name: "Jane")
+ # end
+ puts "Done adding required data"
+ end
+end
diff --git a/fixtures/src_template__sec_tester/expected/tasks/db/seed/sample_data.cr b/fixtures/src_template__sec_tester/expected/tasks/db/seed/sample_data.cr
new file mode 100644
index 00000000..231d7e8d
--- /dev/null
+++ b/fixtures/src_template__sec_tester/expected/tasks/db/seed/sample_data.cr
@@ -0,0 +1,30 @@
+require "../../../spec/support/factories/**"
+
+# Add sample data helpful for development, e.g. (fake users, blog posts, etc.)
+#
+# Use `Db::Seed::RequiredData` if you need to create data *required* for your
+# app to work.
+class Db::Seed::SampleData < LuckyTask::Task
+ summary "Add sample database records helpful for development"
+
+ def call
+ # Using an Avram::Factory:
+ #
+ # Use the defaults, but override just the email
+ # UserFactory.create &.email("me@example.com")
+
+ # Using a SaveOperation:
+ # ```
+ # SignUpUser.create!(email: "me@example.com", password: "test123", password_confirmation: "test123")
+ # ```
+ #
+ # You likely want to be able to run this file more than once. To do that,
+ # only create the record if it doesn't exist yet:
+ # ```
+ # if UserQuery.new.email("me@example.com").none?
+ # SignUpUser.create!(email: "me@example.com", password: "test123", password_confirmation: "test123")
+ # end
+ # ```
+ puts "Done adding sample data"
+ end
+end
diff --git a/script/test b/script/test
index 363859c5..329124b5 100755
--- a/script/test
+++ b/script/test
@@ -16,13 +16,14 @@ else
printf "\nCrystal format checks passed.\n\n"
fi
-if ! crystal ./bin/ameba.cr; then
- printf "\nCode did not pass Ameba checks.\n"
- printf "\nResolve Ameba linter errors, then run this checker again\n\n"
- exit 1
-else
- printf "\nAmeba linter checks passed.\n\n"
-fi
+# TODO: check for ambea in PATH
+#if ! crystal ./bin/ameba.cr; then
+ #printf "\nCode did not pass Ameba checks.\n"
+ #printf "\nResolve Ameba linter errors, then run this checker again\n\n"
+ #exit 1
+#else
+ #printf "\nAmeba linter checks passed.\n\n"
+#fi
printf "\nRunning specs with 'crystal spec'\n\n"
crystal spec "$@"
diff --git a/shard.yml b/shard.yml
index 3b8129fb..7e0bb45b 100644
--- a/shard.yml
+++ b/shard.yml
@@ -22,8 +22,3 @@ dependencies:
nox:
github: crystal-loot/nox
version: ~> 0.2.2
-
-development_dependencies:
- ameba:
- github: crystal-ameba/ameba
- version: ~> 1.5.0
diff --git a/spec/generators_spec.cr b/spec/generators_spec.cr
new file mode 100644
index 00000000..255e9d03
--- /dev/null
+++ b/spec/generators_spec.cr
@@ -0,0 +1,195 @@
+require "./spec_helper2"
+
+describe "Generators" do
+ around_each do |example|
+ with_tempfile("tmp") do |tmp|
+ Dir.mkdir_p(tmp)
+ Dir.cd(tmp) do
+ example.run
+ end
+ end
+ end
+
+ describe ApiAuthenticationTemplate do
+ it "generates api authentication template" do
+ generate_snapshot("api_authentication_template") do
+ ApiAuthenticationTemplate.new
+ end
+ end
+ end
+
+ describe AppWithSecTesterTemplate do
+ it "generates app with sec tester template" do
+ generate_snapshot("app_sec_tester_template") do
+ AppWithSecTesterTemplate.new(
+ generate_auth: false,
+ browser: false
+ )
+ end
+ end
+
+ it "generates app with sec tester template with only generate auth option" do
+ generate_snapshot("app_sec_tester_template__generate_auth") do
+ AppWithSecTesterTemplate.new(
+ generate_auth: true,
+ browser: false
+ )
+ end
+ end
+
+ it "generates app with sec tester template with only browser option" do
+ generate_snapshot("app_sec_tester_template__browser") do
+ AppWithSecTesterTemplate.new(
+ generate_auth: false,
+ browser: true
+ )
+ end
+ end
+ end
+
+ describe ShardFileGenerator do
+ it "generates shard file template" do
+ generate_snapshot("shard_file_template") do
+ ShardFileGenerator.new(
+ "test-shard",
+ generate_auth: true,
+ browser: true,
+ with_sec_tester: true
+ ).tap do |instance|
+ instance.crystal_version = "1.6.2"
+ end
+ end
+ end
+
+ it "generates shard file template with only browser option" do
+ generate_snapshot("shard_file_template__browser") do
+ ShardFileGenerator.new(
+ "test-shard",
+ generate_auth: false,
+ browser: true,
+ with_sec_tester: false
+ ).tap do |instance|
+ instance.crystal_version = "1.6.2"
+ end
+ end
+ end
+
+ it "generates shard file template with only generate auth option" do
+ generate_snapshot("shard_file_template__generate_auth") do
+ ShardFileGenerator.new(
+ "test-shard",
+ generate_auth: true,
+ browser: false,
+ with_sec_tester: false
+ ).tap do |instance|
+ instance.crystal_version = "1.6.2"
+ end
+ end
+ end
+
+ it "generates shard file template with only sec tester option" do
+ generate_snapshot("shard_file_template__with_sec_tester") do
+ ShardFileGenerator.new(
+ "test-shard",
+ generate_auth: false,
+ browser: false,
+ with_sec_tester: true
+ ).tap do |instance|
+ instance.crystal_version = "1.6.2"
+ end
+ end
+ end
+ end
+
+ describe SrcTemplate do
+ it "generates src template" do
+ generate_snapshot("src_template") do
+ SrcTemplate.new(
+ "test-project",
+ generate_auth: true,
+ api_only: true,
+ with_sec_tester: true
+ ).tap do |instance|
+ instance.secret_key_base = "1234567890"
+ instance.crystal_version = "1.6.2"
+ instance.lucky_cli_version = "1.0.0"
+ end
+ end
+ end
+
+ it "generates src template with only generate auth option" do
+ generate_snapshot("src_template__generate_auth") do
+ SrcTemplate.new(
+ "test-project",
+ generate_auth: true,
+ api_only: false,
+ with_sec_tester: false
+ ).tap do |instance|
+ instance.secret_key_base = "1234567890"
+ instance.crystal_version = "1.6.2"
+ instance.lucky_cli_version = "1.0.0"
+ end
+ end
+ end
+
+ it "generates src template with only api option" do
+ generate_snapshot("src_template__api_only") do
+ SrcTemplate.new(
+ "test-project",
+ generate_auth: false,
+ api_only: true,
+ with_sec_tester: false
+ ).tap do |instance|
+ instance.secret_key_base = "1234567890"
+ instance.crystal_version = "1.6.2"
+ instance.lucky_cli_version = "1.0.0"
+ end
+ end
+ end
+
+ it "generates src template with only sec tester option" do
+ generate_snapshot("src_template__sec_tester") do
+ SrcTemplate.new(
+ "test-project",
+ generate_auth: false,
+ api_only: false,
+ with_sec_tester: true
+ ).tap do |instance|
+ instance.secret_key_base = "1234567890"
+ instance.crystal_version = "1.6.2"
+ instance.lucky_cli_version = "1.0.0"
+ end
+ end
+ end
+ end
+
+ describe BaseAuthenticationSrcTemplate do
+ it "generates base authentication src template" do
+ generate_snapshot("base_authentication_src_template") do
+ BaseAuthenticationSrcTemplate.new
+ end
+ end
+ end
+
+ describe BrowserAuthenticationSrcTemplate do
+ it "generates browser authentication src template" do
+ generate_snapshot("browser_authentication_src_template") do
+ BrowserAuthenticationSrcTemplate.new
+ end
+ end
+ end
+
+ describe BrowserSrcTemplate do
+ it "generates browser src template" do
+ generate_snapshot("browser_src_template") do
+ BrowserSrcTemplate.new(generate_auth: true)
+ end
+ end
+
+ it "generates browser src template without generate auth" do
+ generate_snapshot("browser_src_template__generate_auth") do
+ BrowserSrcTemplate.new(generate_auth: false)
+ end
+ end
+ end
+end
diff --git a/spec/spec_helper2.cr b/spec/spec_helper2.cr
new file mode 100644
index 00000000..3717428d
--- /dev/null
+++ b/spec/spec_helper2.cr
@@ -0,0 +1,43 @@
+require "spec"
+require "lucky_template/spec"
+require "./support/tempfile"
+require "../src/lucky_cli"
+
+include LuckyTemplate::Spec
+
+SPEC_UPDATE_SNAPSHOT = ENV["SPEC_UPDATE_SNAPSHOT"]? == "1"
+
+def generate_snapshot(fixture_name, file = __FILE__, line = __LINE__, &)
+ generator = yield
+
+ actual_path = Path[Dir.current]
+ expected_path = Path["#{__DIR__}/../fixtures"] / fixture_name / "expected"
+
+ if SPEC_UPDATE_SNAPSHOT
+ FileUtils.rm_rf(expected_path)
+ FileUtils.mkdir_p(expected_path)
+ generator.render(expected_path)
+ end
+
+ FileUtils.mkdir_p(actual_path)
+ generator.render(actual_path)
+
+ generator.template_folder.should be_valid_at(actual_path), file: file, line: line
+
+ snapshot = LuckyTemplate.snapshot(generator.template_folder)
+ snapshot.select { |_, type| type.file? }.each do |filename, _|
+ actual_filename = actual_path / filename
+ expected_filename = expected_path / filename
+
+ unless File.same_content?(actual_filename, expected_filename)
+ actual_lines = File.read_lines(actual_filename)
+ expected_lines = File.read_lines(expected_filename)
+
+ actual_lines.each_with_index do |actual_line, index|
+ actual_line.should eq(expected_lines[index]), file: file, line: line
+ end
+ end
+ end
+
+ generator
+end
diff --git a/spec/support/tempfile.cr b/spec/support/tempfile.cr
new file mode 100644
index 00000000..d683a4db
--- /dev/null
+++ b/spec/support/tempfile.cr
@@ -0,0 +1,61 @@
+# https://github.com/crystal-lang/crystal/blob/master/spec/support/tempfile.cr
+
+require "file_utils"
+
+SPEC_TEMPFILE_PATH = File.join(Dir.tempdir, "cr-spec-#{Random.new.hex(4)}")
+SPEC_TEMPFILE_CLEANUP = ENV["SPEC_TEMPFILE_CLEANUP"]? != "0"
+
+# Expands *paths* in a unique temp folder and yield them to the block.
+#
+# The *paths* are interpreted relative to a unique folder for every spec run and
+# prefixed by the name of the spec file that requests them.
+#
+# The constructed path is yielded to the block and cleaned up afterwards.
+#
+# Paths should still be uniquely chosen inside a spec file. This helper
+# ensures they're placed in the temporary location (`Dir.tempdir`),
+# avoids name clashes between parallel spec runs and cleans up afterwards.
+#
+# The unique directory for the spec run is removed `at_exit`.
+#
+# If the environment variable `SPEC_TEMPFILE_CLEANUP` is set to `0`, no paths
+# will be cleaned up, enabling easier debugging.
+def with_tempfile(*paths, file = __FILE__, &)
+ calling_spec = File.basename(file).rchop("_spec.cr")
+ paths = paths.map { |path| File.join(SPEC_TEMPFILE_PATH, calling_spec, path) }
+ FileUtils.mkdir_p(File.join(SPEC_TEMPFILE_PATH, calling_spec))
+
+ begin
+ yield *paths
+ ensure
+ if SPEC_TEMPFILE_CLEANUP
+ paths.each do |path|
+ rm_rf(path) if File.exists?(path)
+ end
+ end
+ end
+end
+
+if SPEC_TEMPFILE_CLEANUP
+ at_exit do
+ rm_rf(SPEC_TEMPFILE_PATH) if Dir.exists?(SPEC_TEMPFILE_PATH)
+ end
+end
+
+private def rm_rf(path : String) : Nil
+ if Dir.exists?(path) && !File.symlink?(path)
+ Dir.each_child(path) do |entry|
+ src = File.join(path, entry)
+ rm_rf(src)
+ end
+ Dir.delete(path)
+ else
+ begin
+ File.delete(path)
+ rescue File::AccessDeniedError
+ # To be able to delete read-only files (e.g. ones under .git/) on Windows.
+ File.chmod(path, 0o666)
+ File.delete(path)
+ end
+ end
+end
diff --git a/src/lucky.cr b/src/lucky.cr
index 0808bde9..fc3fed3f 100755
--- a/src/lucky.cr
+++ b/src/lucky.cr
@@ -1,5 +1,6 @@
require "colorize"
require "option_parser"
+require "ecr"
require "./lucky_cli/spinner"
require "./lucky_cli"
require "./generators/*"
diff --git a/src/lucky_cli.cr b/src/lucky_cli.cr
index 97f114bc..87d20b50 100644
--- a/src/lucky_cli.cr
+++ b/src/lucky_cli.cr
@@ -1,6 +1,7 @@
require "lucky_template"
require "lucky_task"
require "nox"
+require "ecr"
require "./lucky_cli/**"
require "./generators/*"
require "./init"
diff --git a/src/lucky_cli/shard_file_generator.cr b/src/lucky_cli/shard_file_generator.cr
index 3e04c045..ebd31d13 100644
--- a/src/lucky_cli/shard_file_generator.cr
+++ b/src/lucky_cli/shard_file_generator.cr
@@ -6,6 +6,7 @@ class ShardFileGenerator
getter? generate_auth : Bool
getter? browser : Bool
getter? with_sec_tester : Bool
+ property(crystal_version) { Crystal::VERSION }
def initialize(
@project_name : String,
@@ -34,7 +35,7 @@ class ShardFileGenerator
"main" => Path.new("src", "#{project_name}.cr").to_s,
},
},
- "crystal" => ">= #{Crystal::VERSION}",
+ "crystal" => ">= #{crystal_version}",
"dependencies" => shard_deps,
"development_dependencies" => shard_dev_deps,
}
diff --git a/src/lucky_cli/src_template.cr b/src/lucky_cli/src_template.cr
index ca052279..49c07f2f 100644
--- a/src/lucky_cli/src_template.cr
+++ b/src/lucky_cli/src_template.cr
@@ -5,8 +5,16 @@ class SrcTemplate
getter project_name
getter? api_only, generate_auth, with_sec_tester
getter crystal_project_name : String
+ property(secret_key_base) { Random::Secure.base64(32) }
+ property(crystal_version) { Crystal::VERSION }
+ property(lucky_cli_version) { LuckyCli::VERSION }
- def initialize(@project_name : String, @generate_auth : Bool, @api_only : Bool, @with_sec_tester : Bool)
+ def initialize(
+ @project_name : String,
+ @generate_auth : Bool,
+ @api_only : Bool,
+ @with_sec_tester : Bool
+ )
@crystal_project_name = @project_name.gsub("-", "_")
end
@@ -18,10 +26,6 @@ class SrcTemplate
!api_only?
end
- def secret_key_base
- Random::Secure.base64(32)
- end
-
def render(path : Path)
LuckyTemplate.write!(path, template_folder)
end
diff --git a/src/web_app_skeleton/.crystal-version.ecr b/src/web_app_skeleton/.crystal-version.ecr
index bc6a0336..5c2537ca 100644
--- a/src/web_app_skeleton/.crystal-version.ecr
+++ b/src/web_app_skeleton/.crystal-version.ecr
@@ -1 +1 @@
-<%= Crystal::VERSION %>
+<%= crystal_version %>
diff --git a/src/web_app_skeleton/.github/workflows/ci.yml.ecr b/src/web_app_skeleton/.github/workflows/ci.yml.ecr
index 5a3113ca..77c7428f 100644
--- a/src/web_app_skeleton/.github/workflows/ci.yml.ecr
+++ b/src/web_app_skeleton/.github/workflows/ci.yml.ecr
@@ -12,7 +12,7 @@ jobs:
fail-fast: false
matrix:
crystal_version:
- - <%= Crystal::VERSION %>
+ - <%= crystal_version %>
experimental:
- false
runs-on: ubuntu-latest
@@ -31,7 +31,7 @@ jobs:
fail-fast: false
matrix:
crystal_version:
- - <%= Crystal::VERSION %>
+ - <%= crystal_version %>
experimental:
- false
runs-on: ubuntu-latest
diff --git a/src/web_app_skeleton/docker/development.dockerfile.ecr b/src/web_app_skeleton/docker/development.dockerfile.ecr
index 5cc00c85..13abd8a5 100644
--- a/src/web_app_skeleton/docker/development.dockerfile.ecr
+++ b/src/web_app_skeleton/docker/development.dockerfile.ecr
@@ -1,4 +1,4 @@
-FROM crystallang/crystal:<%= Crystal::VERSION %>
+FROM crystallang/crystal:<%= crystal_version %>
# Install utilities required to make this Dockerfile run
RUN apt-get update && \
@@ -33,7 +33,7 @@ RUN apt-get update && \
# Install lucky cli
WORKDIR /lucky/cli
RUN git clone https://github.com/luckyframework/lucky_cli . && \
- git checkout v<%= LuckyCli::VERSION %> && \
+ git checkout v<%= lucky_cli_version %> && \
shards build --without-development && \
cp bin/lucky /usr/bin