gem 'hashie-forbidden_attributes'
gem 'paranoia', '~> 2.0'
gem 'paperclip', '~> 4.3'
+gem 'backport_new_renderer'
gem 'http'
gem 'addressable'
descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0)
thread_safe (~> 0.3, >= 0.3.1)
+ backport_new_renderer (1.0.0)
+ rails
better_errors (2.1.1)
coderay (>= 1.0.0)
erubis (>= 2.6.6)
DEPENDENCIES
addressable
+ backport_new_renderer
better_errors
binding_of_caller
coffee-rails (~> 4.1.0)
+++ /dev/null
-module Mastodon
- class API < Grape::API
- rescue_from :all
-
- mount Mastodon::Ostatus
- mount Mastodon::Rest
- end
-end
+++ /dev/null
-module Mastodon
- module Entities
- class Account < Grape::Entity
- include ApplicationHelper
-
- expose :id
- expose :username
-
- expose :domain do |account|
- account.local? ? LOCAL_DOMAIN : account.domain
- end
-
- expose :display_name
- expose :note
-
- expose :url do |account|
- account.local? ? profile_url(name: account.username) : account.url
- end
- end
-
- class Status < Grape::Entity
- include ApplicationHelper
-
- format_with(:iso_timestamp) { |dt| dt.iso8601 }
-
- expose :id
-
- expose :uri do |status|
- status.local? ? unique_tag(status.stream_entry.created_at, status.stream_entry.activity_id, status.stream_entry.activity_type) : status.uri
- end
-
- expose :url do |status|
- status.local? ? status_url(name: status.account.username, id: status.id) : status.url
- end
-
- expose :text
- expose :in_reply_to_id
-
- expose :reblog_of_id
- expose :reblog, using: Mastodon::Entities::Status
-
- expose :account, using: Mastodon::Entities::Account
-
- with_options(format_with: :iso_timestamp) do
- expose :created_at
- expose :updated_at
- end
- end
-
- class StreamEntry < Grape::Entity
- expose :activity, using: Mastodon::Entities::Status
- end
- end
-end
+++ /dev/null
-module Mastodon
- class Ostatus < Grape::API
- format :txt
-
- before do
- @account = Account.find(params[:id])
- end
-
- resource :subscriptions do
- helpers do
- include ApplicationHelper
- end
-
- desc 'Receive updates from an account'
-
- params do
- requires :id, type: String, desc: 'Account ID'
- end
-
- post ':id' do
- body = request.body.read
-
- if @account.subscription(subscription_url(@account)).verify(body, env['HTTP_X_HUB_SIGNATURE'])
- ProcessFeedService.new.(body, @account)
- status 201
- else
- status 202
- end
- end
-
- desc 'Confirm PuSH subscription to an account'
-
- params do
- requires :id, type: String, desc: 'Account ID'
- requires 'hub.topic', type: String, desc: 'Topic URL'
- requires 'hub.verify_token', type: String, desc: 'Verification token'
- requires 'hub.challenge', type: String, desc: 'Hub challenge'
- end
-
- get ':id' do
- if @account.subscription(subscription_url(@account)).valid?(params['hub.topic'], params['hub.verify_token'])
- params['hub.challenge']
- else
- error! :not_found, 404
- end
- end
- end
-
- resource :salmon do
- desc 'Receive Salmon updates targeted to account'
-
- params do
- requires :id, type: String, desc: 'Account ID'
- end
-
- post ':id' do
- ProcessInteractionService.new.(request.body.read, @account)
- status 201
- end
- end
- end
-end
+++ /dev/null
-module Mastodon
- class Rest < Grape::API
- version 'v1', using: :path
- format :json
-
- helpers do
- def current_user
- User.first
- end
- end
-
- resource :timelines do
- desc 'Return a public timeline'
-
- get :public do
- # todo
- end
-
- desc 'Return the home timeline of a logged in user'
-
- get :home do
- present current_user.timeline, with: Mastodon::Entities::StreamEntry
- end
-
- desc 'Return the notifications timeline of a logged in user'
-
- get :notifications do
- # todo
- end
- end
-
- resource :accounts do
- desc 'Return a user profile'
-
- params do
- requires :id, type: String, desc: 'Account ID'
- end
-
- get ':id' do
- present Account.find(params[:id]), with: Mastodon::Entities::Account
- end
- end
- end
-end
+++ /dev/null
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://coffeescript.org/
+++ /dev/null
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://coffeescript.org/
+++ /dev/null
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://coffeescript.org/
+++ /dev/null
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://coffeescript.org/
--- /dev/null
+.card {
+ display: flex;
+ background: $primary-color;
+ box-shadow: 4px 3px 0 rgba(0, 0, 0, 0.1);
+
+ .bio {
+ flex-grow: 1;
+ }
+
+ .name {
+ font-size: 20px;
+ line-height: 18px * 1.5;
+ color: $quaternary-color;
+
+ small {
+ display: block;
+ font-size: 14px;
+ color: $quaternary-color;
+ }
+ }
+
+ .avatar {
+ width: 96px;
+ float: left;
+ margin-right: 10px;
+ padding: 10px;
+ padding-right: 0;
+ padding-left: 9px;
+ margin-top: -30px;
+
+ img {
+ width: 94px;
+ height: 94px;
+ display: block;
+ border-radius: 5px;
+ box-shadow: 4px 3px 0 rgba(0, 0, 0, 0.1);
+ }
+ }
+}
-// Place all the styles related to the Atom controller here.
+// Place all the styles related to the API::Salmon controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
-// Place all the styles related to the XRD controller here.
+// Place all the styles related to the API::Subscriptions controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
}
.container {
- width: 800px;
+ width: 700px;
margin: 0 auto;
margin-top: 40px;
}
@import 'home';
-@import 'profile';
+@import 'accounts';
+@import 'stream_entries';
-.card {
- display: flex;
- background: $darker-background-color;
- border: 1px solid darken($darker-background-color, 15%);
- box-shadow: 4px 3px 0 rgba(0, 0, 0, 0.1);
-
- .bio {
- flex-grow: 1;
- }
-
- .name {
- font-size: 24px;
- line-height: 18px * 1.5;
- color: $text-color;
-
- small {
- display: block;
- font-size: 14px;
- color: $lighter-text-color;
- }
- }
-
- .avatar {
- width: 96px;
- float: left;
- margin-right: 10px;
- padding: 10px;
- padding-left: 9px;
- margin-top: -30px;
-
- img {
- width: 94px;
- height: 94px;
- display: block;
- border: 2px solid $lighter-text-color;
- border-radius: 5px;
- }
- }
-}
.activity-stream {
clear: both;
box-shadow: 4px 3px 0 rgba(0, 0, 0, 0.1);
.entry {
- border-bottom: 1px solid darken($background-color, 10%);
+ border-bottom: 1px solid $darker-background-color;
background: $background-color;
border-left: 2px solid $primary-color;
&.entry-reblog {
border-left: 2px solid $tertiary-color;
+
+ .content {
+ a {
+ color: $tertiary-color;
+ }
+ }
}
&.entry-predecessor, &.entry-successor {
border-left: 2px solid $lighter-text-color;
background: darken($background-color, 5%);
+
+ .content {
+ a {
+ color: $lighter-text-color;
+ }
+ }
}
&.entry-follow, &.entry-favourite {
}
.header {
- margin-bottom: 10px;
+ margin-bottom: 5px;
padding: 10px;
padding-bottom: 0;
+ padding-left: 8px;
.name {
text-decoration: none;
}
.pre-header {
- border-bottom: 1px solid darken($background-color, 10%);
+ border-bottom: 1px solid darken($background-color, 5%);
color: $tertiary-color;
padding: 5px 10px;
padding-left: 8px;
}
.content {
- font-size: 16px;
+ font-size: 14px;
padding: 0 10px;
padding-left: 8px;
+ padding-bottom: 25px;
a {
color: $primary-color;
text-decoration: underline;
}
}
-
- .counters {
- margin-top: 15px;
- color: $lighter-text-color;
- cursor: default;
- padding: 10px;
- padding-top: 0;
-
- .counter {
- display: inline-block;
- margin-right: 10px;
- color: $lighter-text-color;
- }
-
- .conversation-link {
- color: $primary-color;
- text-decoration: underline;
- float: right;
- }
- }
}
--- /dev/null
+class AccountsController < ApplicationController
+ before_action :set_account
+
+ def show
+ respond_to do |format|
+ format.html
+ format.atom
+ end
+ end
+
+ private
+
+ def set_account
+ @account = Account.find_by!(username: params[:username], domain: nil)
+ end
+end
--- /dev/null
+class Api::SalmonController < ApplicationController
+ before_action :set_account
+
+ def update
+ ProcessInteractionService.new.(request.body.read, @account)
+ render nothing: true, status: 201
+ end
+
+ private
+
+ def set_account
+ @account = Account.find(params[:id])
+ end
+end
--- /dev/null
+class Api::SubscriptionsController < ApplicationController
+ before_action :set_account
+
+ def show
+ if @account.subscription(api_subscription_url(@account.id)).valid?(params['hub.topic'], params['hub.verify_token'])
+ render text: params['hub.challenge'], status: 200
+ else
+ render nothing: true, status: 404
+ end
+ end
+
+ def update
+ body = request.body.read
+
+ if @account.subscription(api_subscription_url(@account.id)).verify(body, env['HTTP_X_HUB_SIGNATURE'])
+ ProcessFeedService.new.(body, @account)
+ render nothing: true, status: 201
+ else
+ render nothing: true, status: 202
+ end
+ end
+
+ private
+
+ def set_account
+ @account = Account.find(params[:id])
+ end
+end
+++ /dev/null
-class AtomController < ApplicationController
- before_filter :set_format
-
- def user_stream
- @account = Account.find_by!(id: params[:id], domain: nil)
- end
-
- def entry
- @entry = StreamEntry.find(params[:id])
- end
-
- private
-
- def set_format
- request.format = 'xml'
- response.headers['Content-Type'] = 'application/atom+xml'
- end
-end
+++ /dev/null
-class ProfileController < ApplicationController
- before_action :set_account
-
- def show
- end
-
- def entry
- @entry = @account.stream_entries.find(params[:id])
- @type = @entry.activity_type.downcase
- end
-
- private
-
- def set_account
- @account = Account.find_by!(username: params[:name], domain: nil)
- end
-end
--- /dev/null
+class StreamEntriesController < ApplicationController
+ before_action :set_account
+ before_action :set_stream_entry
+
+ def show
+ @type = @stream_entry.activity_type.downcase
+
+ respond_to do |format|
+ format.html
+ format.atom
+ end
+ end
+
+ private
+
+ def set_account
+ @account = Account.find_by!(username: params[:account_username], domain: nil)
+ end
+
+ def set_stream_entry
+ @stream_entry = @account.stream_entries.find(params[:id])
+ end
+end
@account = Account.find_by!(username: username_from_resource, domain: nil)
@canonical_account_uri = "acct:#{@account.username}@#{LOCAL_DOMAIN}"
@magic_key = pem_to_magic_key(@account.keypair.public_key)
+ rescue ActiveRecord::RecordNotFound
+ render nothing: true, status: 404
end
private
--- /dev/null
+module AccountsHelper
+
+end
--- /dev/null
+module Api::SalmonHelper
+end
--- /dev/null
+module Api::SubscriptionsHelper
+end
module ApplicationHelper
- include RoutingHelper
-
def unique_tag(date, id, type)
"tag:#{LOCAL_DOMAIN},#{date.strftime('%Y-%m-%d')}:objectId=#{id}:objectType=#{type}"
end
def local_id?(id)
id.start_with?("tag:#{LOCAL_DOMAIN}")
end
-
- def subscription_url(account)
- add_base_url_prefix subscriptions_path(id: account.id, format: '')
- end
-
- def salmon_url(account)
- add_base_url_prefix salmon_path(id: account.id, format: '')
- end
-
- def profile_url(account)
- account.local? ? super(name: account.username) : account.url
- end
-
- def status_url(status)
- status.local? ? super(name: status.account.username, id: status.stream_entry.id) : status.url
- end
-
- def add_base_url_prefix(suffix)
- File.join(root_url, "api", suffix)
- end
end
-module AtomHelper
+module AtomBuilderHelper
def stream_updated_at
@account.stream_entries.last ? (@account.updated_at > @account.stream_entries.last.created_at ? @account.updated_at : @account.stream_entries.last.created_at) : @account.updated_at
end
xml['thr'].send('in-reply-to', { ref: uri, href: url, type: 'text/html' })
end
- def disambiguate_uri(target)
+ def uri_for_target(target)
if target.local?
if target.object_type == :person
- profile_url(target)
+ account_url(target)
else
unique_tag(target.stream_entry.created_at, target.stream_entry.activity_id, target.stream_entry.activity_type)
end
end
end
- def disambiguate_url(target)
+ def url_for_target(target)
if target.local?
if target.object_type == :person
- profile_url(target)
+ account_url(target)
else
- status_url(target)
+ account_stream_entry_url(target.account, target.stream_entry)
end
else
target.url
end
def link_mention(xml, account)
- xml.link(rel: 'mentioned', href: disambiguate_uri(account))
+ xml.link(rel: 'mentioned', href: uri_for_target(account))
end
def link_avatar(xml, account)
- xml.link('rel' => 'avatar', 'type' => account.avatar_content_type, 'media:width' => '300', 'media:height' =>'300', 'href' => asset_url(account.avatar.url(:large)))
- xml.link('rel' => 'avatar', 'type' => account.avatar_content_type, 'media:width' => '96', 'media:height' =>'96', 'href' => asset_url(account.avatar.url(:medium)))
- xml.link('rel' => 'avatar', 'type' => account.avatar_content_type, 'media:width' => '48', 'media:height' =>'48', 'href' => asset_url(account.avatar.url(:small)))
+ xml.link('rel' => 'avatar', 'type' => account.avatar_content_type, 'media:width' => '300', 'media:height' =>'300', 'href' => asset_url(account.avatar.url(:large, false)))
+ xml.link('rel' => 'avatar', 'type' => account.avatar_content_type, 'media:width' => '96', 'media:height' =>'96', 'href' => asset_url(account.avatar.url(:medium, false)))
+ xml.link('rel' => 'avatar', 'type' => account.avatar_content_type, 'media:width' => '48', 'media:height' =>'48', 'href' => asset_url(account.avatar.url(:small, false)))
end
def logo(xml, url)
def include_author(xml, account)
object_type xml, :person
- uri xml, profile_url(account)
+ uri xml, url_for_target(account)
name xml, account.username
summary xml, account.note
- link_alternate xml, profile_url(account)
+ link_alternate xml, url_for_target(account)
link_avatar xml, account
portable_contact xml, account
end
title xml, stream_entry.title
content xml, stream_entry.content
verb xml, stream_entry.verb
- link_self xml, atom_entry_url(id: stream_entry.id)
+ link_self xml, account_stream_entry_url(stream_entry.account, stream_entry, format: 'atom')
object_type xml, stream_entry.object_type
# Comments need thread element
if stream_entry.threaded?
- in_reply_to xml, disambiguate_uri(stream_entry.thread), disambiguate_url(stream_entry.thread)
+ in_reply_to xml, uri_for_target(stream_entry.thread), url_for_target(stream_entry.thread)
end
if stream_entry.targeted?
target(xml) do
object_type xml, stream_entry.target.object_type
- simple_id xml, disambiguate_uri(stream_entry.target)
+ simple_id xml, uri_for_target(stream_entry.target)
title xml, stream_entry.target.title
- link_alternate xml, disambiguate_url(stream_entry.target)
+ link_alternate xml, url_for_target(stream_entry.target)
# People have summary and portable contacts information
if stream_entry.target.object_type == :person
-module ProfileHelper
+module StreamEntriesHelper
def display_name(account)
account.display_name.blank? ? account.username : account.display_name
end
@avatar_remote_url = url
end
+ def to_param
+ self.username
+ end
+
before_create do
if local?
keypair = OpenSSL::PKey::RSA.new(Rails.env.test? ? 1024 : 2048)
class User < ActiveRecord::Base
belongs_to :account, inverse_of: :user
-
validates :account, presence: true
-
- def timeline
- StreamEntry.where(account_id: self.account.following, activity_type: 'Status').order('id desc')
- end
end
class BaseService
+ include RoutingHelper
include ApplicationHelper
end
account.secret = SecureRandom.hex
account.verify_token = SecureRandom.hex
- subscription = account.subscription(subscription_url(account))
+ subscription = account.subscription(api_subscription_url(account.id))
subscription.subscribe
account.save!
follow = source_account.follow!(target_account)
send_interaction_service.(follow.stream_entry, target_account)
- source_account.ping!(atom_user_stream_url(id: source_account.id), [HUB_URL])
+ source_account.ping!(account_url(account, format: 'atom'), [HUB_URL])
end
private
def call(account, text, in_reply_to = nil)
status = account.statuses.create!(text: text, thread: in_reply_to)
process_mentions_service.(status)
- account.ping!(atom_user_stream_url(id: account.id), [HUB_URL])
+ account.ping!(account_url(account, format: 'atom'), [HUB_URL])
status
end
end
def mentions_account?(xml, account)
- xml.xpath('/xmlns:entry/xmlns:link[@rel="mentioned"]').each { |mention_link| return true if mention_link.attribute('href').value == profile_url(account) }
+ xml.xpath('/xmlns:entry/xmlns:link[@rel="mentioned"]').each { |mention_link| return true if mention_link.attribute('href').value == url_for_target(account) }
false
end
# @return [Status]
def call(account, reblogged_status)
reblog = account.statuses.create!(reblog: reblogged_status, text: '')
- account.ping!(atom_user_stream_url(id: account.id), [HUB_URL])
+ account.ping!(account_url(account, format: 'atom'), [HUB_URL])
return reblog if reblogged_status.local?
send_interaction_service.(reblog.stream_entry, reblogged_status.account)
reblog
class SendInteractionService < BaseService
- include AtomHelper
+ include AtomBuilderHelper
# Send an Atom representation of an interaction to a remote Salmon endpoint
# @param [StreamEntry] stream_entry
Nokogiri::XML::Builder.new do |xml|
feed(xml) do
- simple_id xml, atom_user_stream_url(id: @account.id)
+ simple_id xml, account_url(@account, format: 'atom')
title xml, @account.display_name
subtitle xml, @account.note
updated_at xml, stream_updated_at
include_author xml, @account
end
- link_alternate xml, profile_url(@account)
- link_self xml, atom_user_stream_url(id: @account.id)
+ link_alternate xml, url_for_target(@account)
+ link_self xml, account_url(@account, format: 'atom')
link_hub xml, HUB_URL
- link_salmon xml, salmon_url(@account)
+ link_salmon xml, api_salmon_url(@account.id)
@account.stream_entries.order('id desc').each do |stream_entry|
entry(xml, false) do
- content_for :header_tags do
- %link{ rel: 'salmon', href: salmon_url(@account) }/
- %link{ rel: 'alternate', type: 'application/atom+xml', href: atom_user_stream_url(id: @account.id) }/
+ %link{ rel: 'salmon', href: api_salmon_url(@account.id) }/
+ %link{ rel: 'alternate', type: 'application/atom+xml', href: account_url(@account, format: 'atom') }/
.card
.avatar= image_tag @account.avatar.url(:medium)
.activity-stream
- @account.statuses.order('id desc').each do |status|
- = render partial: 'status', locals: { status: status, include_threads: false, is_successor: false, is_predecessor: false }
+ = render partial: 'stream_entries/status', locals: { status: status, include_threads: false, is_successor: false, is_predecessor: false }
+++ /dev/null
-- if status.reply?
- = link_to "In response to #{status.thread.account.acct}", status_url(status.thread), class: 'conversation-link'
+++ /dev/null
-= link_to profile_url(status.account), class: 'name' do
- %strong= display_name(status.account)
- = "@#{status.account.acct}"
-
-= link_to status_url(status), class: 'time' do
- %span{ title: status.created_at }
- = relative_time(status.created_at)
+++ /dev/null
-- content_for :header_tags do
- %link{ rel: 'alternate', type: 'application/atom+xml', href: atom_entry_url(id: @entry.id) }/
-
-.activity-stream
- = render partial: @type, locals: { @type.to_sym => @entry.activity, include_threads: true, is_predecessor: false, is_successor: false }
.pre-header
%i.fa.fa-retweet
Shared by
- = link_to display_name(status.account), profile_url(status.account), class: 'name'
+ = link_to display_name(status.account), url_for_target(status.account), class: 'name'
+
.entry__container
.avatar
= image_tag avatar_for_status_url(status)
+
.entry__container__container
.header
- = render partial: 'status_header', locals: { status: status.reblog? ? status.reblog : status }
+ = link_to url_for_target(status.reblog? ? status.reblog.account : status.account), class: 'name' do
+ %strong= display_name(status.reblog? ? status.reblog.account : status.account)
+ = "@#{status.reblog? ? status.reblog.account.acct : status.account.acct}"
+ = link_to url_for_target(status.reblog? ? status.reblog : status), class: 'time' do
+ %span{ title: status.reblog? ? status.reblog.created_at : status.created_at }
+ = relative_time(status.reblog? ? status.reblog.created_at : status.created_at)
+
.content
= status.content.html_safe
- .counters
- = render partial: 'status_footer', locals: { status: status.reblog? ? status.reblog : status }
- if include_threads
- status.replies.each do |status|
--- /dev/null
+- content_for :header_tags do
+ %link{ rel: 'alternate', type: 'application/atom+xml', href: account_stream_entry_url(@account, @stream_entry, format: 'atom') }/
+
+.activity-stream
+ = render partial: @type, locals: { @type.to_sym => @stream_entry.activity, include_threads: true, is_predecessor: false, is_successor: false }
Nokogiri::XML::Builder.new do |xml|
xml.XRD(xmlns: 'http://docs.oasis-open.org/ns/xri/xrd-1.0') do
xml.Subject @canonical_account_uri
- xml.Alias profile_url(@account)
- xml.Link(rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: profile_url(@account))
+ xml.Alias url_for_target(@account)
+ xml.Link(rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: url_for_target(@account))
xml.Link(rel: 'http://schemas.google.com/g/2010#updates-from', type: 'application/atom+xml', href: atom_user_stream_url(id: @account.id))
- xml.Link(rel: 'salmon', href: salmon_url(@account))
+ xml.Link(rel: 'salmon', href: api_salmon_url(@account.id))
xml.Link(rel: 'magic-public-key', href: @magic_key)
end
end.to_xml
get '.well-known/host-meta', to: 'xrd#host_meta', as: :host_meta
get '.well-known/webfinger', to: 'xrd#webfinger', as: :webfinger
- get 'atom/entries/:id', to: 'atom#entry', as: :atom_entry
- get 'atom/users/:id', to: 'atom#user_stream', as: :atom_user_stream
- get 'users/:name', to: 'profile#show', as: :profile
- get 'users/:name/:id', to: 'profile#entry', as: :status
+ resources :accounts, path: 'users', only: [:show], param: :username do
+ resources :stream_entries, path: 'updates', only: [:show]
+ end
- mount Mastodon::API => '/api/'
+ namespace :api do
+ resources :subscriptions, only: [:show]
+ post '/subscriptions/:id', to: 'subscriptions#update'
+ post '/salmon/:id', to: 'salmon#update', as: :salmon
+ end
root 'home#index'
end
--- /dev/null
+require 'rails_helper'
+
+RSpec.describe AccountsController, type: :controller do
+ let(:alice) { Fabricate(:account, username: 'alice') }
+
+ describe 'GET #show' do
+ it 'returns 200' do
+ get :show, username: alice.username
+ expect(response).to have_http_status(:success)
+ end
+
+ it 'returns 200 with Atom' do
+ get :show, username: alice.username, format: 'atom'
+ expect(response).to have_http_status(:success)
+ end
+ end
+end
--- /dev/null
+require 'rails_helper'
+
+RSpec.describe Api::SalmonController, type: :controller do
+ describe 'POST #update' do
+ pending
+ end
+end
--- /dev/null
+require 'rails_helper'
+
+RSpec.describe Api::SubscriptionsController, type: :controller do
+ describe 'GET #show' do
+ pending
+ end
+
+ describe 'POST #update' do
+ pending
+ end
+end
+++ /dev/null
-require 'rails_helper'
-
-RSpec.describe AtomController, type: :controller do
- describe 'GET #user_stream' do
- pending
- end
-
- describe 'GET #entry' do
- pending
- end
-end
RSpec.describe HomeController, type: :controller do
describe 'GET #index' do
- pending
+ it 'returns 200' do
+ get :index
+ expect(response).to have_http_status(:success)
+ end
end
end
+++ /dev/null
-require 'rails_helper'
-
-RSpec.describe ProfileController, type: :controller do
- describe 'GET #show' do
- pending
- end
-
- describe 'GET #entry' do
- pending
- end
-end
--- /dev/null
+require 'rails_helper'
+
+RSpec.describe StreamEntriesController, type: :controller do
+ let(:alice) { Fabricate(:account, username: 'alice') }
+ let(:status) { Fabricate(:status, account: alice) }
+
+ describe 'GET #show' do
+ it 'returns 200 with HTML' do
+ get :show, account_username: alice.username, id: status.stream_entry.id
+ expect(response).to have_http_status(:success)
+ end
+
+ it 'returns 200 with Atom' do
+ get :show, account_username: alice.username, id: status.stream_entry.id, format: 'atom'
+ expect(response).to have_http_status(:success)
+ end
+ end
+end
RSpec.describe XrdController, type: :controller do
describe 'GET #host_meta' do
- pending
+ it 'returns 200' do
+ get :host_meta
+ expect(response).to have_http_status(:success)
+ end
end
describe 'GET #webfinger' do
- pending
+ let(:alice) { Fabricate(:account, username: 'alice') }
+
+ it 'returns 200 when account can be found' do
+ get :webfinger, resource: "acct:#{alice.username}@anything.com"
+ expect(response).to have_http_status(:success)
+ end
+
+ it 'returns 404 when account cannot be found' do
+ get :webfinger, resource: 'acct:not@existing.com'
+ expect(response).to have_http_status(:not_found)
+ end
end
end
--- /dev/null
+require 'rails_helper'
+
+# Specs in this file have access to a helper object that includes
+# the AccountsHelper. For example:
+#
+# describe AccountsHelper do
+# describe "string concat" do
+# it "concats two strings with spaces" do
+# expect(helper.concat_strings("this","that")).to eq("this that")
+# end
+# end
+# end
+RSpec.describe AccountsHelper, type: :helper do
+ pending "add some examples to (or delete) #{__FILE__}"
+end
--- /dev/null
+require 'rails_helper'
+
+# Specs in this file have access to a helper object that includes
+# the Api::SalmonHelper. For example:
+#
+# describe Api::SalmonHelper do
+# describe "string concat" do
+# it "concats two strings with spaces" do
+# expect(helper.concat_strings("this","that")).to eq("this that")
+# end
+# end
+# end
+RSpec.describe Api::SalmonHelper, type: :helper do
+ pending "add some examples to (or delete) #{__FILE__}"
+end
--- /dev/null
+require 'rails_helper'
+
+# Specs in this file have access to a helper object that includes
+# the Api::SubscriptionsHelper. For example:
+#
+# describe Api::SubscriptionsHelper do
+# describe "string concat" do
+# it "concats two strings with spaces" do
+# expect(helper.concat_strings("this","that")).to eq("this that")
+# end
+# end
+# end
+RSpec.describe Api::SubscriptionsHelper, type: :helper do
+ pending "add some examples to (or delete) #{__FILE__}"
+end
expect(helper.local_id?('tag:foreign.tld;objectId=12:objectType=Status')).to be false
end
end
-
- describe '#add_base_url_prefix' do
- it 'returns full API URL from base to suffix' do
- expect(helper.add_base_url_prefix('test')).to eql "#{root_url}api/test"
- end
- end
-
- describe '#profile_url' do
- pending
- end
-
- describe '#status_url' do
- pending
- end
end
require 'rails_helper'
-RSpec.describe AtomHelper, type: :helper do
+RSpec.describe AtomBuilderHelper, type: :helper do
describe '#stream_updated_at' do
pending
end
require 'rails_helper'
-RSpec.describe ProfileHelper, type: :helper do
+RSpec.describe StreamEntriesHelper, type: :helper do
describe '#display_name' do
pending
end