Skip to content

Commit

Permalink
chore: clean up code base to improve selfhosting (#49)
Browse files Browse the repository at this point in the history
  • Loading branch information
shafy authored Mar 13, 2022
1 parent c48d207 commit 240b5be
Show file tree
Hide file tree
Showing 12 changed files with 191 additions and 111 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,5 @@ jobs:
bundle exec rails test
env:
STRIPE_ENDPOINT_SECRET: ${{ secrets.STRIPE_ENDPOINT_SECRET }}
ALLOW_REGISTRATION: true
ALLOW_REGISTRATION: true
FUGU_CLOUD: true
7 changes: 7 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,10 @@ rails db:create db:migrate

## Starting the server
To start the server, run `./bin/dev` or run `rails s` and `rails tailwindcss:watch` as separate processes (the first command does basically the same with `foreman`). This is necessary because we need the [Tailwind](https://github.com/rails/tailwindcss-rails) watch process during development.


## Fugu Cloud

There are a few places in the codebase where you will encounter logic that is only needed for Fugu Cloud, the hosted solution by us. These parts of the code are not needed if you are self-hosting, and probably also not relevant for you as a contributor. For the most part this concerns code that relates to payment and subscriptions.

Currently, we use the environment variable `FUGU_CLOUD` to determine if the instance is being self-hosted or not. Later on, we might improve this structure by factoring out the main code base to a Rails Engine.
25 changes: 2 additions & 23 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
class ApplicationController < ActionController::Base
before_action :authenticate_user!

include InactiveAlertable

private

def show_test_alert
Expand All @@ -23,27 +25,4 @@ def authorize_project_user

return redirect_to projects_path unless current_user == @project.user
end

def show_not_active_flash
flash.now[:not_active] = user_canceled_flash if current_user.canceled?
flash.now[:not_active] = user_inactive_flash if current_user.inactive?
end

# rubocop:disable Rails/OutputSafety
def user_canceled_flash
%(
You have canceled your subscription and it will end soon.
Make sure to &nbsp; <a href="#{users_settings_path}">renew</a> &nbsp; it
if you want to keep using Fugu.
).html_safe
end

def user_inactive_flash
%(
Hey there 👋 Make sure to&nbsp;<a href="#{users_settings_path}">subscribe</a>&nbsp;in
order to track events.
You can use your test API key to give Fugu a spin without a subscription.
).html_safe
end
# rubocop:enable Rails/OutputSafety
end
33 changes: 33 additions & 0 deletions app/controllers/concerns/inactive_alertable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

# These alerts are only used in the Fugu Cloud veresion (not imporatnt if you're self-hosting)
module InactiveAlertable
extend ActiveSupport::Concern

private

def show_not_active_flash
return unless ENV["FUGU_CLOUD"] == "true"

flash.now[:not_active] = user_canceled_flash if current_user.canceled?
flash.now[:not_active] = user_inactive_flash if current_user.inactive?
end

# rubocop:disable Rails/OutputSafety
def user_canceled_flash
%(
You have canceled your subscription and it will end soon.
Make sure to &nbsp; <a href="#{users_settings_path}">renew</a> &nbsp; it
if you want to keep using Fugu.
).html_safe
end

def user_inactive_flash
%(
Hey there 👋 Make sure to&nbsp;<a href="#{users_settings_path}">subscribe</a>&nbsp;in
order to track events.
You can use your test API key to give Fugu a spin without a subscription.
).html_safe
end
# rubocop:enable Rails/OutputSafety
end
3 changes: 3 additions & 0 deletions app/controllers/users/settings_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ class SettingsController < ApplicationController
before_action :show_not_active_flash, only: %i[show]

def show
return unless ENV["FUGU_CLOUD"] == "true"

# following code is irrelevant if you're self-hosting
return unless current_user.canceled? && current_user.stripe_customer_id.present?

customer = retrieve_stripe_customer
Expand Down
20 changes: 20 additions & 0 deletions app/models/concerns/inactivable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

# This validation is only run for the Fugu Cloud version (does not apply for self-hosting)
module Inactivable
extend ActiveSupport::Concern

included do
validate :user_cannot_be_inactive, if: -> { ENV["FUGU_CLOUD"] == "true" }
end

private

def user_cannot_be_inactive
return unless api_key

return unless !api_key.test && api_key.project.user.inactive?

errors.add(:base, "You need an active subscription to capture events with your live API key")
end
end
11 changes: 2 additions & 9 deletions app/models/event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ class Event < ApplicationRecord
message: "'%{value}' is a reserved event name by Fugu and can't be used"
}

validate :user_cannot_be_inactive
include Inactivable

validate :excluded_property_values
validate :excluded_property_names
validate :limit_property_name_length
Expand Down Expand Up @@ -249,14 +250,6 @@ def titleize_name
self.name = name.downcase.titleize if name
end

def user_cannot_be_inactive
return unless api_key

return unless !api_key.test && api_key.project.user.inactive?

errors.add(:base, "You need an active subscription to capture events with your live API key")
end

def excluded_property_names
excluded_names = %w[all email e-mail e_mail ip ip-address ip_address address phone phone-number
phone_number]
Expand Down
28 changes: 28 additions & 0 deletions app/views/users/settings/_subscription.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<%# Only needed by Fugu Cloud %>

<h2>Subscription</h2>
<p class="mb-4">
<% if current_user.active? %>
Your Fugu subscription is active and renews monthly.<br>
<%= link_to "Cancel subscription or update payment method", stripe_customer_portal_url,
data: { turbo: false }
%>
<% elsif current_user.canceled? %>
Your Fugu subscription is canceled, but will remain active until <%= cancel_at %>.<br>
<%= link_to "Renew subscription", stripe_customer_portal_url,
data: { turbo: false }
%>
<% else %>
You don't have an active subscription.<br>
Fugu costs $9/month and includes 1 million events/month.<br>
<%= link_to "Start your subscription now", stripe_checkout_session_url,
data: { turbo: false }
%>
<% if current_user.stripe_customer_id.present? %>
<br>
<%= link_to "View past invoices", stripe_customer_portal_url,
data: { turbo: false }
%>
<% end %>
<% end %>
</p>
8 changes: 8 additions & 0 deletions app/views/users/settings/_support.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<%# Only needed by Fugu Cloud %>

<h2>Support</h2>
<p class="mb-4">
Thanks for being an early Fugu user ❤️
Contact me if you have questions or need help: [email protected] or
<a href="https://twitter.com/canolcer" targt="_blank">@canolcer</a>.
</p>
39 changes: 6 additions & 33 deletions app/views/users/settings/show.html.erb
Original file line number Diff line number Diff line change
@@ -1,43 +1,16 @@
<% content_for :main do %>
<h1 class="text-xl mb-4 font-medium mb-8">Account settings</h1>
<h1 class="text-xl font-medium mb-8">Account settings</h1>
<%= render "partials/narrow_box" do %>
<h2>Email</h2>
<p class="mb-4">
<%= current_user.email %>
</p>
<h2>Subscription</h2>
<p class="mb-4">
<% if current_user.active? %>
Your Fugu subscription is active and renews monthly.<br>
<%= link_to "Cancel subscription or update payment method", stripe_customer_portal_url,
data: { turbo: false }
%>
<% elsif current_user.canceled? %>
Your Fugu subscription is canceled, but will remain active until <%= @cancel_at %>.<br>
<%= link_to "Renew subscription", stripe_customer_portal_url,
data: { turbo: false }
%>
<% else %>
You don't have an active subscription.<br>
Fugu costs $9/month and includes 1 million events/month.<br>
<%= link_to "Start your subscription now", stripe_checkout_session_url,
data: { turbo: false }
%>
<% if current_user.stripe_customer_id.present? %>
<br>
<%= link_to "View past invoices", stripe_customer_portal_url,
data: { turbo: false }
%>
<% end %>
<% end %>
</p>
<h2>Support</h2>
<p class="mb-4">
Thanks for being an early Fugu user ❤️
Contact me if you have questions or need help: [email protected] or
<a href="https://twitter.com/canolcer" targt="_blank">@canolcer</a>.
</p>

<%# only needed by Fugu Cloud (not needed if you're self-hosting) %>
<%= render partial: "subscription", locals: { cancel_at: @cancel_at } if ENV["FUGU_CLOUD"] == "true" %>
<%# only needed by Fugu Cloud (not needed if you're self-hosting) %>
<%= render partial: "support" if ENV["FUGU_CLOUD"] == "true" %>
<%= button_to "Log out",
destroy_user_session_path,
method: :delete,
Expand Down
124 changes: 80 additions & 44 deletions test/controllers/api/v1/events_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,57 +3,93 @@
require "test_helper"

class EventsControllerTest < ActionDispatch::IntegrationTest
setup do
api_key = FactoryBot.create(:api_key)

@name = "Test Event"
@properties = %({
"color": "Blue",
"size": "12"
})

@event_params = {
api_key: api_key.key_value,
name: @name,
properties: @properties
}
end
class PostCreate < EventsControllerTest
setup do
@user = FactoryBot.create(:user)
project = FactoryBot.create(:project, user: @user)
@api_key_test = FactoryBot.create(:api_key, project: project, test: true)
@api_key_live = FactoryBot.create(:api_key, project: project, test: false)

test "POST create should respond with success" do
post api_v1_events_path, params: @event_params
assert_response :success
end
@name = "Test Event"
@properties = %({
"color": "Blue",
"size": "12"
})

@event_params = {
api_key: @api_key_live.key_value,
name: @name,
properties: @properties
}
end

test "POST create should add event" do
assert_difference("Event.count") do
test "POST create should respond with success" do
post api_v1_events_path, params: @event_params
assert_response :success
end
end

test "POST create should create correct event" do
post api_v1_events_path, params: @event_params
assert_not_empty(Event.where(name: @name))
assert_not_empty(Event.where("properties->>'color' = 'Blue'"))
end
test "POST create should add event" do
assert_difference("Event.count") do
post api_v1_events_path, params: @event_params
end
end

test "POST create should respond with ArgumentError for name = nil" do
@event_params[:name] = nil
post api_v1_events_path, params: @event_params
assert_response 422
assert_match("'name' can't be nil", @response.body)
end
test "POST create should create correct event" do
post api_v1_events_path, params: @event_params
assert_not_empty(Event.where(name: @name))
assert_not_empty(Event.where("properties->>'color' = 'Blue'"))
end

test "POST create should respond with ArgumentError for missing name key" do
@event_params.delete(:name)
post api_v1_events_path, params: @event_params
assert_response 422
assert_match("missing 'name' key", @response.body)
end
test "POST create should respond with ArgumentError for name = nil" do
@event_params[:name] = nil
post api_v1_events_path, params: @event_params
assert_response 422
assert_match("'name' can't be nil", @response.body)
end

test "POST create should respond with JSON error" do
@event_params[:properties] = "{incorrect_json: hehe}"
post api_v1_events_path, params: @event_params
assert_response 422
assert_match("Properties must be valid JSON", @response.body)
test "POST create should respond with ArgumentError for missing name key" do
@event_params.delete(:name)
post api_v1_events_path, params: @event_params
assert_response 422
assert_match("missing 'name' key", @response.body)
end

test "POST create should respond with JSON error" do
@event_params[:properties] = "{incorrect_json: hehe}"
post api_v1_events_path, params: @event_params
assert_response 422
assert_match("Properties must be valid JSON", @response.body)
end

test "POST create should not create event for inactive user" do
@user.update(status: "inactive")
assert_no_difference "Event.count" do
post api_v1_events_path, params: @event_params
end
end

test "POST create should return correct ArgumentError for inactive user" do
@user.update(status: "inactive")
post api_v1_events_path, params: @event_params
assert_response 422
assert_match("You need an active subscription", @response.body)
end

test "POST create should be successful for inactive user if self-hosted" do
ENV["FUGU_CLOUD"] = "false"
@user.update(status: "inactive")
assert_difference "Event.count" do
post api_v1_events_path, params: @event_params
end
ENV["FUGU_CLOUD"] = "true"
end

test "POST create should add event with test api key for inactive user" do
@user.update(status: "inactive")
@event_params[:api_key] = @api_key_test.key_value
assert_difference("Event.count") do
post api_v1_events_path, params: @event_params
end
end
end
end
1 change: 0 additions & 1 deletion test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ class IntegrationTest
end

# Database Cleaner

DatabaseCleaner.strategy = :transaction

module AroundEachTest
Expand Down

0 comments on commit 240b5be

Please sign in to comment.