stoor 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +24 -20
- data/bin/stoor +1 -1
- data/config.ru +50 -16
- data/lib/stoor.rb +1 -2
- data/lib/stoor/git_config.rb +11 -9
- data/lib/stoor/github_auth.rb +10 -10
- data/lib/stoor/transform_content.rb +87 -0
- data/lib/stoor/version.rb +1 -1
- data/spec/lib/stoor/git_config_spec.rb +18 -0
- data/spec/lib/stoor/github_auth_spec.rb +74 -0
- data/spec/lib/stoor/transform_content_spec.rb +146 -0
- data/spec/repo.rb +18 -0
- data/spec/spec_helper.rb +41 -0
- data/stoor.gemspec +5 -2
- metadata +48 -9
- data/lib/stoor/add_after.rb +0 -44
- data/lib/stoor/decorate.rb +0 -63
- data/spec/lib/stoor/add_after_spec.rb +0 -99
data/README.md
CHANGED
@@ -1,17 +1,19 @@
|
|
1
|
-
|
1
|
+
Stoor provides a Gollum (wiki) server with a few other bells and whistles, such as authentication against GitHub OAuth.
|
2
2
|
|
3
3
|
## Rationale
|
4
4
|
|
5
5
|
In our environment, the contents of our wiki are confidential and should not be pushed up to GitHub. We keep the wiki on
|
6
6
|
a local machine (behind a firewall), and put the wiki contents into a directory that is sync'd to box.com. (box.com is useful for confidential
|
7
|
-
data, because they will sign a HIPAA BAA.) Meanwhile, we'd like to authorize access by some means
|
7
|
+
data, because they will sign a HIPAA BAA.) Meanwhile, we'd like to authorize access by some means: we like GitHub Oauth,
|
8
8
|
so that we can constrain access by GitHub Organization Team membership.
|
9
9
|
|
10
10
|
## Requirements
|
11
11
|
|
12
12
|
Ruby 1.9.2 or greater.
|
13
13
|
|
14
|
-
|
14
|
+
An operating system other than Windows (because Gollum doesn't work on Windows, because grit doesn't work on Windows . . .).
|
15
|
+
|
16
|
+
Unfortunately, Stoor will no longer work on Ruby 1.8.7, because `gollum-lib` now wants Nokogiri 1.6.0 ([see?](https://github.com/gollum/gollum-lib/commit/eeb0a4a036001c7621d173e7152b91ed02b21ed0#commitcomment-4170065)), and
|
15
17
|
1.8.7 isn't supported. That's too bad, because it was nice that this would work on the system Ruby on a Mac.
|
16
18
|
|
17
19
|
## Setup
|
@@ -37,9 +39,6 @@ the values for the GitHub commit will be what you see in `git config -l`).
|
|
37
39
|
|
38
40
|
The `stoor` command is a thin wrapper around the `thin` web server, and takes all `thin` options (`-p <port>`, etc.).
|
39
41
|
|
40
|
-
If you get the error `Gollum::InvalidGitRepositoryError` it means that you didn't change your directory to
|
41
|
-
a git repo.
|
42
|
-
|
43
42
|
If you don't have a repo yet for your wiki . . .
|
44
43
|
|
45
44
|
mkdir mywiki
|
@@ -49,9 +48,9 @@ If you don't have a repo yet for your wiki . . .
|
|
49
48
|
|
50
49
|
### Specify the Wiki repo location
|
51
50
|
|
52
|
-
|
51
|
+
STOOR_WIKI_PATH=/Users/admin/wiki stoor
|
53
52
|
|
54
|
-
The `
|
53
|
+
The `STOOR_WIKI_PATH` environment variable provides for locating the wiki contents in a differet repo from the
|
55
54
|
Stoor application. It is strongly advised that you do this so that you can keep your wiki code and wiki
|
56
55
|
content separate.
|
57
56
|
|
@@ -59,15 +58,20 @@ content separate.
|
|
59
58
|
|
60
59
|
Require authorization via GitHub to the GitHub application with the given client id and secret
|
61
60
|
|
62
|
-
|
61
|
+
STOOR_GITHUB_CLIENT_ID=780ec06a331b4f61a345 STOOR_GITHUB_CLIENT_SECRET=f1e5439aff166c34f707747120acbf66ef233fc2 stoor
|
63
62
|
|
64
63
|
Access to the wiki will first run through GitHub OAuth against the app specified by the id and secret. For information
|
65
64
|
on setting up an application in GitHub and obtaining its id and secret, see <https://github.com/settings/applications/new>.
|
66
|
-
If you are running Stoor on localhost with
|
65
|
+
If you are running Stoor on localhost with the `stoor` command, the typical settings would be:
|
67
66
|
|
68
|
-
|
69
|
-
|
70
|
-
|
67
|
+
<table>
|
68
|
+
<tr>
|
69
|
+
<th> Application Name </th> <th> Main URL </th> <th> Callback URL </th>
|
70
|
+
</tr>
|
71
|
+
<tr>
|
72
|
+
<td> YourAppName </td> <td> http://localhost:3000 </td><td> http://localhost:3000/auth/github/callback </td>
|
73
|
+
</tr>
|
74
|
+
</table>
|
71
75
|
|
72
76
|
**NOTE:** No matter what your domain and port, the callback path must be `/auth/github/callback`.
|
73
77
|
|
@@ -78,11 +82,11 @@ application settings.
|
|
78
82
|
|
79
83
|
If there is more than one email associated with the GitHub user, prefer the one from the specified domain (otherwise the first email will be used)
|
80
84
|
|
81
|
-
|
85
|
+
STOOR_GITHUB_EMAIL_DOMAIN=7fff.com STOOR_GITHUB_CLIENT_ID=780ec06a331b4f61a345 STOOR_GITHUB_CLIENT_SECRET=f1e5439aff166c34f707747120acbf66ef233fc2 stoor
|
82
86
|
|
83
87
|
### Require GitHub team
|
84
88
|
|
85
|
-
|
89
|
+
STOOR_GITHUB_TEAM_ID=11155 STOOR_GITHUB_CLIENT_ID=780ec06a331b4f61a345 STOOR_GITHUB_CLIENT_SECRET=f1e5439aff166c34f707747120acbf66ef233fc2 stoor
|
86
90
|
|
87
91
|
If the user is not a member of the specified team, they aren't allowed access.
|
88
92
|
|
@@ -105,7 +109,7 @@ If the user is not a member of the specified team, they aren't allowed access.
|
|
105
109
|
## How I run it
|
106
110
|
|
107
111
|
I like having my own personal wiki. Since Apache is ubiquitous on Macs, I run the Wiki with configuration in `/etc/apache2/httpd.conf`,
|
108
|
-
|
112
|
+
and some Ruby provided by rbenv, and Passenger.
|
109
113
|
|
110
114
|
I create an extra name for 127.0.0.1 in `/etc/hosts` such as `wiki.local`. Then:
|
111
115
|
|
@@ -121,12 +125,12 @@ Then in `/etc/apache2/httpd.conf`:
|
|
121
125
|
NameVirtualHost *:80
|
122
126
|
|
123
127
|
<VirtualHost *:80>
|
124
|
-
SetEnv
|
125
|
-
SetEnv
|
126
|
-
SetEnv
|
128
|
+
SetEnv STOOR_GITHUB_CLIENT_ID 780ec06a331b4f61a345
|
129
|
+
SetEnv STOOR_GITHUB_CLIENT_SECRET f1e5439aff166c34f707747120acbf66ef233fc2
|
130
|
+
SetEnv STOOR_GITHUB_EMAIL_DOMAIN 7fff.com
|
127
131
|
SetEnv STOOR_DOMAIN wiki.local
|
128
132
|
SetEnv STOOR_EXPIRE_AFTER 60
|
129
|
-
SetEnv
|
133
|
+
SetEnv STOOR_WIKI_PATH /Users/jgn/Dropbox/wiki
|
130
134
|
ServerName wiki.local
|
131
135
|
DocumentRoot "/opt/boxen/rbenv/versions/1.9.2-p320/lib/ruby/gems/1.9.1/gems/stoor-0.1.4/public"
|
132
136
|
<Directory "/opt/boxen/rbenv/versions/1.9.2-p320/lib/ruby/gems/1.9.1/gems/stoor-0.1.4/public">
|
data/bin/stoor
CHANGED
data/config.ru
CHANGED
@@ -23,6 +23,11 @@ module Rack
|
|
23
23
|
end
|
24
24
|
|
25
25
|
ENV['RACK_ENV'] ||= 'development'
|
26
|
+
|
27
|
+
domain = ENV['STOOR_DOMAIN'] || 'localhost'
|
28
|
+
secret = ENV['STOOR_SECRET'] || 'stoor'
|
29
|
+
expire_after = (ENV['STOOR_EXPIRE_AFTER'] || '3600').to_i
|
30
|
+
|
26
31
|
log_frag = "#{File.dirname(__FILE__)}/log/#{ENV['RACK_ENV']}"
|
27
32
|
access_logger = Logger.new("#{log_frag}_access.log")
|
28
33
|
access_logger.instance_eval do
|
@@ -32,24 +37,53 @@ access_logger.level = Logger::INFO
|
|
32
37
|
log_stream = File.open("#{log_frag}.log", 'a+')
|
33
38
|
log_stream.sync = true
|
34
39
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
+
gollum_path = ENV['STOOR_WIKI_PATH'] || File.expand_path(File.dirname(__FILE__))
|
41
|
+
repo_exists = true
|
42
|
+
begin
|
43
|
+
Gollum::Wiki.new(gollum_path)
|
44
|
+
rescue Gollum::InvalidGitRepositoryError
|
45
|
+
repo_exists = false
|
46
|
+
message = "Sorry, #{gollum_path} is not a git repository; you might try `cd #{gollum_path}; git init .`."
|
47
|
+
rescue NameError
|
48
|
+
repo_exists = false
|
49
|
+
message = "Sorry, #{gollum_path} doesn't exist; set the environment variable STOOR_WIKI_PATH to point to a git repository."
|
50
|
+
end
|
40
51
|
|
52
|
+
use Rack::Session::Cookie, :domain => domain, :key => 'rack.session', :secret => secret, :expire_after => expire_after
|
41
53
|
use Rack::CommonLogger, access_logger
|
42
54
|
use Stoor::Logger, log_stream, Logger::INFO
|
43
|
-
|
55
|
+
if repo_exists
|
56
|
+
use Stoor::GithubAuth
|
57
|
+
use Stoor::GitConfig, gollum_path
|
58
|
+
use Stoor::TransformContent,
|
59
|
+
pass_condition: ->(request) { request.session['gollum.author'].nil? },
|
60
|
+
regexp: /(<div id="footer">)(.*?)(<\/div>)/im,
|
61
|
+
before: ->(request) do
|
62
|
+
<<-HTML
|
63
|
+
<div style="float: left;">
|
64
|
+
HTML
|
65
|
+
end,
|
66
|
+
after: ->(request) do
|
67
|
+
<<-HTML
|
68
|
+
</div>
|
69
|
+
<div style="float: right;">
|
70
|
+
<p style="text-align: right; font-size: .9em; line-height: 1.6em; color: #999; margin: 0.9em 0;">
|
71
|
+
Commiting as <b>#{request.session['gollum.author'][:name]}</b> (#{request.session['gollum.author'][:email]})#{" | <a href='/logout'>Logout</a>" if request.session['stoor.github.authorized']}
|
72
|
+
</p>
|
73
|
+
</div>
|
74
|
+
HTML
|
75
|
+
end
|
76
|
+
if ENV['STOOR_WIDE']
|
77
|
+
use Stoor::TransformContent,
|
78
|
+
regexp: /<body>/,
|
79
|
+
after: '<style type="text/css">#wiki-wrapper { width: 90%; } .markdown-body table { width: 100%; }</style>'
|
80
|
+
end
|
44
81
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
82
|
+
Precious::App.set(:gollum_path, gollum_path)
|
83
|
+
Precious::App.set(:default_markup, :markdown)
|
84
|
+
Precious::App.set(:wiki_options, { :universal_toc =>false })
|
85
|
+
run Precious::App
|
86
|
+
else
|
87
|
+
run Proc.new { |env| [ 200, { 'Content-Type' => 'text/plain' }, [ message ] ] }
|
88
|
+
puts message
|
50
89
|
end
|
51
|
-
|
52
|
-
Precious::App.set(:gollum_path, wiki_path)
|
53
|
-
Precious::App.set(:default_markup, :markdown)
|
54
|
-
Precious::App.set(:wiki_options, { :universal_toc =>false })
|
55
|
-
run Precious::App
|
data/lib/stoor.rb
CHANGED
data/lib/stoor/git_config.rb
CHANGED
@@ -1,24 +1,26 @@
|
|
1
1
|
module Stoor
|
2
2
|
class GitConfig
|
3
|
-
|
3
|
+
attr_reader :repo_path
|
4
|
+
|
5
|
+
def initialize(app, repo_path = nil)
|
6
|
+
@app, @repo_path = app, repo_path
|
7
|
+
end
|
4
8
|
|
5
9
|
def call(env)
|
6
10
|
@request = Rack::Request.new(env)
|
7
11
|
unless @request.session['gollum.author']
|
8
|
-
if
|
9
|
-
|
10
|
-
if email = git_option_value('user.email')
|
11
|
-
@request.session['gollum.author'] = { :name => name, :email => email }
|
12
|
-
end
|
13
|
-
end
|
12
|
+
if repo_path && (name = git_option_value('user.name')) && (email = git_option_value('user.email'))
|
13
|
+
@request.session['gollum.author'] = { :name => name, :email => email }
|
14
14
|
end
|
15
15
|
end
|
16
16
|
@app.call(env)
|
17
17
|
end
|
18
18
|
|
19
|
+
private
|
20
|
+
|
19
21
|
def git_option_value(option)
|
20
|
-
|
21
|
-
|
22
|
+
@repo ||= Grit::Repo.new(repo_path)
|
23
|
+
@repo.config[option]
|
22
24
|
end
|
23
25
|
end
|
24
26
|
end
|
data/lib/stoor/github_auth.rb
CHANGED
@@ -2,9 +2,9 @@ module Stoor
|
|
2
2
|
class GithubAuth < Sinatra::Base
|
3
3
|
|
4
4
|
set :github_options, {
|
5
|
-
:scopes => "user
|
6
|
-
:client_id => ENV['
|
7
|
-
:secret => ENV['
|
5
|
+
:scopes => "user:email",
|
6
|
+
:client_id => ENV['STOOR_GITHUB_CLIENT_ID'],
|
7
|
+
:secret => ENV['STOOR_GITHUB_CLIENT_SECRET']
|
8
8
|
}
|
9
9
|
|
10
10
|
register Sinatra::Auth::Github
|
@@ -16,23 +16,23 @@ module Stoor
|
|
16
16
|
end
|
17
17
|
|
18
18
|
get '/*' do
|
19
|
-
|
19
|
+
session['stoor.github.authorized'] = nil
|
20
20
|
|
21
|
-
pass unless ENV['
|
21
|
+
pass unless ENV['STOOR_GITHUB_CLIENT_ID'] && ENV['STOOR_GITHUB_CLIENT_SECRET']
|
22
22
|
|
23
23
|
pass if request.path_info =~ /\./
|
24
24
|
|
25
25
|
authenticate!
|
26
|
-
if ENV['
|
27
|
-
github_team_authenticate!(ENV['
|
26
|
+
if ENV['STOOR_GITHUB_TEAM_ID']
|
27
|
+
github_team_authenticate!(ENV['STOOR_GITHUB_TEAM_ID'])
|
28
28
|
end
|
29
29
|
|
30
|
-
|
30
|
+
session['stoor.github.authorized'] = 'yes'
|
31
31
|
|
32
32
|
email = nil
|
33
33
|
emails = github_user.api.emails
|
34
|
-
if ENV['
|
35
|
-
email = emails.find { |e| e =~ /#{ENV['
|
34
|
+
if ENV['STOOR_GITHUB_EMAIL_DOMAIN']
|
35
|
+
email = emails.find { |e| e =~ /#{ENV['STOOR_GITHUB_EMAIL_DOMAIN']}/ }
|
36
36
|
end
|
37
37
|
email ||= emails.first
|
38
38
|
session['gollum.author'] = { :name => github_user.name, :email => email }
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'rack'
|
2
|
+
|
3
|
+
module Stoor
|
4
|
+
class TransformContent
|
5
|
+
include Rack::Utils
|
6
|
+
|
7
|
+
attr_reader :status, :headers, :request
|
8
|
+
attr_reader :app, :regexp, :content_type, :pass_condition
|
9
|
+
|
10
|
+
def initialize(app, options)
|
11
|
+
@app = app
|
12
|
+
@regexp = options[:regexp]
|
13
|
+
@before = options[:before]
|
14
|
+
@after = options[:after]
|
15
|
+
@content_type = options[:content_type] || 'text/html'
|
16
|
+
@pass_condition = options[:pass_condition] || ->(env) { false }
|
17
|
+
end
|
18
|
+
|
19
|
+
def call(env)
|
20
|
+
@status, @headers, response = @app.call(env)
|
21
|
+
@request = Rack::Request.new(env)
|
22
|
+
|
23
|
+
if pass_condition.call(request)
|
24
|
+
if request.logger
|
25
|
+
request.logger.info "Skipping -- because pass condition met"
|
26
|
+
end
|
27
|
+
else
|
28
|
+
@headers = HeaderHash.new(@headers)
|
29
|
+
if has_body && not_encoded && headers['content-type'] && headers['content-type'].index(content_type)
|
30
|
+
content = Array(response).join('')
|
31
|
+
if match_data = content.match(regexp)
|
32
|
+
new_body = interpolate(match_data)
|
33
|
+
headers['Content-Length'] = new_body.bytesize.to_s
|
34
|
+
return [status, headers, [new_body]]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
[status, headers, response]
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def has_body
|
45
|
+
!STATUS_WITH_NO_ENTITY_BODY.include?(status)
|
46
|
+
end
|
47
|
+
|
48
|
+
def not_encoded
|
49
|
+
!headers['transfer-encoding']
|
50
|
+
end
|
51
|
+
|
52
|
+
def interpolate(match_data)
|
53
|
+
# regexp: /<body>/
|
54
|
+
# result: before<body>after
|
55
|
+
# regexp: /(<div>)(.*?)(<\/div)
|
56
|
+
# result: <div>beforezzzafter</div>
|
57
|
+
"".tap do |s|
|
58
|
+
s << match_data.pre_match
|
59
|
+
if match_data.size == 1
|
60
|
+
s << before + match_data[0] + after
|
61
|
+
elsif match_data.size == 4
|
62
|
+
s << match_data[1] + before + match_data[2] + after + match_data[3]
|
63
|
+
else
|
64
|
+
# Might just set the result to content?
|
65
|
+
raise "Unexpected number of captures in #{match_data.regexp}"
|
66
|
+
end
|
67
|
+
s << match_data.post_match
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def before
|
72
|
+
freshen(@before)
|
73
|
+
end
|
74
|
+
|
75
|
+
def after
|
76
|
+
freshen(@after)
|
77
|
+
end
|
78
|
+
|
79
|
+
def freshen(e)
|
80
|
+
if e.respond_to? :call
|
81
|
+
e.call(request)
|
82
|
+
else
|
83
|
+
e.to_s
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
data/lib/stoor/version.rb
CHANGED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'stoor/git_config'
|
2
|
+
require 'spec_helper'
|
3
|
+
require 'grit'
|
4
|
+
require 'repo'
|
5
|
+
|
6
|
+
module Stoor
|
7
|
+
describe GitConfig do
|
8
|
+
let(:inner_app) { ->(env) { [200, { }, []] } }
|
9
|
+
let(:app) { GitConfig.new(inner_app, 'repo') }
|
10
|
+
|
11
|
+
include_context 'repo'
|
12
|
+
|
13
|
+
it 'finds git config user.name and user.email and puts them into the session under the key gollum.author' do
|
14
|
+
get '/'
|
15
|
+
expect(last_request.env['rack.session']['gollum.author']).to eq(name: 'Mortimer Snerd', email: 'snerd@example.com')
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'sinatra'
|
3
|
+
require 'mustache'
|
4
|
+
require 'sinatra/auth/github'
|
5
|
+
require 'sinatra/auth/github/test/test_helper'
|
6
|
+
require 'gollum/app'
|
7
|
+
require 'stoor/github_auth'
|
8
|
+
require 'stoor/views/layout'
|
9
|
+
require 'stoor/views/logout'
|
10
|
+
require 'repo'
|
11
|
+
|
12
|
+
module Stoor
|
13
|
+
|
14
|
+
# Follows https://github.com/atmos/sinatra_auth_github/blob/master/spec/login_spec.rb
|
15
|
+
# To run these specs, define a testing app at https://github.com/settings/applications/new,
|
16
|
+
# with a callback URL such as http://localhost:9393/auth/github/callback
|
17
|
+
# Then
|
18
|
+
# STOOR_GITHUB_CLIENT_ID=xxx STOOR_GITHUB_CLIENT_SECRET=yyy stoor -p 9393
|
19
|
+
# Authenticate.
|
20
|
+
# Now you can test:
|
21
|
+
# STOOR_GITHUB_CLIENT_ID=xxx STOOR_GITHUB_CLIENT_SECRET=yyy STOOR_TESTING_USER=jgn bundle exec rspec
|
22
|
+
|
23
|
+
describe "Logged in users" do
|
24
|
+
include Stoor::Test::Helper
|
25
|
+
|
26
|
+
let(:inner_app) { ->(env) { [200, { }, [ 'protected info' ]] } }
|
27
|
+
let(:sessions) { Rack::Session::Cookie.new(inner_app, secret: 'boo') }
|
28
|
+
let(:app) { GithubAuth.new(sessions) }
|
29
|
+
|
30
|
+
include_context 'repo'
|
31
|
+
ENV['STOOR_WIKI_PATH'] = './repo'
|
32
|
+
|
33
|
+
before do
|
34
|
+
Stoor::GithubAuth.send(:enable, :sessions)
|
35
|
+
@user = make_user(ENV['STOOR_TESTING_USER'], [ 'effie@example.com', 'effie@7fff.com', 'john@7fff.com' ])
|
36
|
+
login_as @user
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'Shows the home page' do
|
40
|
+
get '/'
|
41
|
+
expect(last_response.body).to match('protected info')
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'Sets the gollum.user with the first email' do
|
45
|
+
get '/'
|
46
|
+
expect(last_request.env['rack.session']['gollum.author']).to eq(name: 'Effie Klinker', email: 'effie@example.com')
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'Sets the gollum.user according to domain if specified' do
|
50
|
+
save = ENV['STOOR_GITHUB_EMAIL_DOMAIN']
|
51
|
+
ENV['STOOR_GITHUB_EMAIL_DOMAIN'] = '7fff.com'
|
52
|
+
get '/'
|
53
|
+
expect(last_request.env['rack.session']['gollum.author']).to eq(name: 'Effie Klinker', email: 'effie@7fff.com')
|
54
|
+
ENV['STOOR_GITHUB_EMAIL_DOMAIN'] = save
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'logs the user out' do
|
58
|
+
get '/'
|
59
|
+
get '/logout'
|
60
|
+
expect(last_response.status).to eql(200)
|
61
|
+
expect(last_response.body).to match("You're logged out")
|
62
|
+
|
63
|
+
get '/'
|
64
|
+
expect(last_response.status).to eql(302)
|
65
|
+
expect(last_response.headers['Location']).to match(/^https:\/\/github\.com\/login\/oauth\/authorize/)
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'shows the securocat when github returns an oauth error' do
|
69
|
+
get '/auth/github/callback?error=redirect_uri_mismatch'
|
70
|
+
follow_redirect!
|
71
|
+
expect(last_response.body).to match(/securocat\.png/)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
require 'stoor/transform_content'
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
module Stoor
|
5
|
+
|
6
|
+
describe TransformContent, 'content-type matching' do
|
7
|
+
context 'default with text/plain response' do
|
8
|
+
let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/plain' }, '<body>'] } }
|
9
|
+
let(:app) { TransformContent.new(inner_app, regexp: /<body>/, after: 'after-text') }
|
10
|
+
|
11
|
+
it "skips" do
|
12
|
+
get '/'
|
13
|
+
expect(last_response.body).to eq('<body>')
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'default with text/html response' do
|
18
|
+
let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/html' }, '<body>'] } }
|
19
|
+
let(:app) { TransformContent.new(inner_app, regexp: /<body>/, after: 'after-text') }
|
20
|
+
|
21
|
+
it "processes" do
|
22
|
+
get '/'
|
23
|
+
expect(last_response.body).to eq('<body>after-text')
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context 'specifying text/plain with text/html response' do
|
28
|
+
let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/html' }, '<body>'] } }
|
29
|
+
let(:app) { TransformContent.new(inner_app, regexp: /<body>/, after: 'after-text', content_type: 'text/plain') }
|
30
|
+
|
31
|
+
it "skips" do
|
32
|
+
get '/'
|
33
|
+
expect(last_response.body).to eq('<body>')
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'specifying regexp with text/html response' do
|
38
|
+
let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/whatever' }, '<body>'] } }
|
39
|
+
let(:app) { TransformContent.new(inner_app, regexp: /<body>/, after: 'after-text', content_type: /\Atext\//) }
|
40
|
+
|
41
|
+
it "processes" do
|
42
|
+
get '/'
|
43
|
+
expect(last_response.body).to eq('<body>after-text')
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe TransformContent, 'passes on condition' do
|
49
|
+
let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/html' }, '<body>'] } }
|
50
|
+
let(:app) { TransformContent.new(inner_app, regexp: /<body>/, after: 'after-text', pass_condition: ->(env) { true }) }
|
51
|
+
|
52
|
+
it 'skips when a passed-in condition returns true' do
|
53
|
+
get '/'
|
54
|
+
expect(last_response.body).to eq('<body>')
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe TransformContent, 'insertion after' do
|
59
|
+
let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/html' }, '<body>'] } }
|
60
|
+
let(:app) { TransformContent.new(inner_app, regexp: /<body>/, after: 'after-text') }
|
61
|
+
|
62
|
+
it 'adds text after the first occurrence of a search string' do
|
63
|
+
get '/'
|
64
|
+
expect(last_response.body).to eq('<body>after-text')
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe TransformContent, 'insertion before' do
|
69
|
+
let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/html' }, '<body>'] } }
|
70
|
+
let(:app) { TransformContent.new(inner_app, regexp: /<body>/, before: 'before-text') }
|
71
|
+
|
72
|
+
it 'adds text before the first occurrence of a search string' do
|
73
|
+
get '/'
|
74
|
+
expect(last_response.body).to eq('before-text<body>')
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe TransformContent, 'insertion before and after' do
|
79
|
+
let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/html' }, '<body>'] } }
|
80
|
+
let(:app) { TransformContent.new(inner_app, regexp: /<body>/, before: 'before-text', after: 'after-text') }
|
81
|
+
|
82
|
+
it 'adds text before and after the first occurrence of a search string' do
|
83
|
+
get '/'
|
84
|
+
expect(last_response.body).to eq('before-text<body>after-text')
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
describe TransformContent, 'insertion before and after with captures' do
|
89
|
+
let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/html' }, '<div>foo</div>'] } }
|
90
|
+
let(:app) { TransformContent.new(inner_app, regexp: /(<div>)(.*?)(<\/div>)/, before: 'before-text', after: 'after-text') }
|
91
|
+
|
92
|
+
it 'adds text before and after the middle capture' do
|
93
|
+
get '/'
|
94
|
+
expect(last_response.body).to eq('<div>before-textfooafter-text</div>')
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
describe TransformContent, 'not 0 or 3 captures' do
|
99
|
+
let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/html' }, '<div>foo</div>'] } }
|
100
|
+
let(:app) { TransformContent.new(inner_app, regexp: /(<div>).*?(<\/div>)/, before: 'before-text', after: 'after-text') }
|
101
|
+
|
102
|
+
it 'raises an exception' do
|
103
|
+
expect { get '/' }.to raise_error
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
describe TransformContent, 'insertion after with two potential matches' do
|
108
|
+
let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/html' }, '<body> <body>'] } }
|
109
|
+
let(:app) { TransformContent.new(inner_app, regexp: /<body>/, after: 'after-text') }
|
110
|
+
|
111
|
+
it 'adds text only after the first occurrence of a search string' do
|
112
|
+
get '/'
|
113
|
+
expect(last_response.body).to eq('<body>after-text <body>')
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
describe TransformContent, 'insertion if response is an Array' do
|
118
|
+
let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/html' }, [ '<bo', 'dy>' ] ] } }
|
119
|
+
let(:app) { TransformContent.new(inner_app, regexp: /<body>/, after: 'after-text') }
|
120
|
+
|
121
|
+
it 'adds text after the first occurrence of a search string' do
|
122
|
+
get '/'
|
123
|
+
expect(last_response.body).to eq('<body>after-text')
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
describe TransformContent, 'status code implies no body' do
|
128
|
+
let(:inner_app) { ->(env) { [204, { 'Content-Type' => 'text/html' }, '<body>' ] } }
|
129
|
+
let(:app) { TransformContent.new(inner_app, regexp: /<body>/, after: 'after-text') }
|
130
|
+
|
131
|
+
it 'skips' do
|
132
|
+
get '/'
|
133
|
+
expect(last_response.body).to eq('<body>')
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
describe TransformContent, 'response is transfer-encoded' do
|
138
|
+
let(:inner_app) { ->(env) { [200, { 'Transfer-Encoding' => 'Chunked', 'Content-Type' => 'text/html' }, '<body>' ] } }
|
139
|
+
let(:app) { TransformContent.new(inner_app, regexp: /<body>/, after: 'after-text') }
|
140
|
+
|
141
|
+
it 'skips' do
|
142
|
+
get '/'
|
143
|
+
expect(last_response.body).to eq('<body>')
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
data/spec/repo.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
shared_context 'repo' do
|
2
|
+
before do
|
3
|
+
repo = Grit::Repo.init('repo')
|
4
|
+
File.open('repo/.git/config', 'a+') do |f|
|
5
|
+
f.write <<-GITCONFIG.strip_heredoc
|
6
|
+
[user]
|
7
|
+
name = Mortimer Snerd
|
8
|
+
email = snerd@example.com
|
9
|
+
GITCONFIG
|
10
|
+
end
|
11
|
+
File.open('repo/Home.md', 'w') { |f| f.write('### Nice wiki page') }
|
12
|
+
repo.add('repo/Home.md')
|
13
|
+
end
|
14
|
+
|
15
|
+
after do
|
16
|
+
FileUtils.rm_rf 'repo'
|
17
|
+
end
|
18
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,5 +1,46 @@
|
|
1
1
|
require 'rack/test'
|
2
|
+
require 'ostruct'
|
2
3
|
|
3
4
|
RSpec.configure do |conf|
|
4
5
|
conf.include Rack::Test::Methods
|
5
6
|
end
|
7
|
+
|
8
|
+
class String
|
9
|
+
def strip_heredoc
|
10
|
+
indent = scan(/^[ \t]*(?=\S)/).min.size || 0
|
11
|
+
gsub(/^[ \t]{#{indent}}/, '')
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
require 'warden/github'
|
16
|
+
require 'warden/test/helpers'
|
17
|
+
require 'warden/github/user'
|
18
|
+
|
19
|
+
# Follows https://github.com/atmos/sinatra_auth_github/blob/master/lib/sinatra/auth/github/test/test_helper.rb
|
20
|
+
module Stoor
|
21
|
+
module Test
|
22
|
+
module Helper
|
23
|
+
include Warden::Test::Helpers
|
24
|
+
|
25
|
+
class User < Warden::GitHub::User
|
26
|
+
attr_accessor :api
|
27
|
+
end
|
28
|
+
|
29
|
+
def make_user(login, emails = 'effie@example.com', override_attributes = {})
|
30
|
+
emails = Array(emails)
|
31
|
+
attributes = {
|
32
|
+
'login' => login,
|
33
|
+
'email' => emails.first,
|
34
|
+
'name' => "Effie Klinker",
|
35
|
+
'company' => "7fff",
|
36
|
+
'gravatar_id' => 'a'*32,
|
37
|
+
'avatar_url' => 'https://a249.e.akamai.net/assets.github.com/images/gravatars/gravatar-140.png?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-user-420.png',
|
38
|
+
}
|
39
|
+
User.new(attributes.merge! override_attributes).tap do |user|
|
40
|
+
user.api = OpenStruct.new(:emails => emails)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/stoor.gemspec
CHANGED
@@ -6,11 +6,12 @@ Gem::Specification.new do |s|
|
|
6
6
|
s.version = Stoor::VERSION
|
7
7
|
s.date = Time.now.utc.strftime('%Y-%m-%d')
|
8
8
|
s.summary = 'Front-end for Gollum'
|
9
|
-
s.description = '
|
9
|
+
s.description = 'Stoor is an app to bring up a local Gollum (wiki software) against a Git repo with bells and whistles like auth.'
|
10
|
+
s.license = 'MIT'
|
10
11
|
s.authors = ['John G. Norman']
|
11
12
|
s.email = 'john@7fff.com'
|
12
13
|
s.files = `git ls-files`.split("\n")
|
13
|
-
s.homepage = 'https://
|
14
|
+
s.homepage = 'https://github.com/jgn/stoor'
|
14
15
|
s.rdoc_options = ['--charset=UTF-8']
|
15
16
|
s.require_paths = ['lib']
|
16
17
|
s.test_files = `git ls-files spec`.split("\n")
|
@@ -18,8 +19,10 @@ Gem::Specification.new do |s|
|
|
18
19
|
s.add_dependency 'gollum', '~> 2.5.0'
|
19
20
|
s.add_dependency 'sinatra_auth_github', '~> 1.0.0'
|
20
21
|
s.add_dependency 'json', '~> 1.8.0'
|
22
|
+
s.add_dependency 'grit', '~> 2.5.0'
|
21
23
|
s.executables << 'stoor'
|
22
24
|
|
25
|
+
s.add_development_dependency 'warden-github'
|
23
26
|
s.add_development_dependency 'rack-test'
|
24
27
|
s.add_development_dependency 'rspec'
|
25
28
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stoor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.5
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-10-01 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: thin
|
@@ -75,6 +75,38 @@ dependencies:
|
|
75
75
|
- - ~>
|
76
76
|
- !ruby/object:Gem::Version
|
77
77
|
version: 1.8.0
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: grit
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ~>
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 2.5.0
|
86
|
+
type: :runtime
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ~>
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 2.5.0
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: warden-github
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
78
110
|
- !ruby/object:Gem::Dependency
|
79
111
|
name: rack-test
|
80
112
|
requirement: !ruby/object:Gem::Requirement
|
@@ -107,7 +139,8 @@ dependencies:
|
|
107
139
|
- - ! '>='
|
108
140
|
- !ruby/object:Gem::Version
|
109
141
|
version: '0'
|
110
|
-
description:
|
142
|
+
description: Stoor is an app to bring up a local Gollum (wiki software) against a
|
143
|
+
Git repo with bells and whistles like auth.
|
111
144
|
email: john@7fff.com
|
112
145
|
executables:
|
113
146
|
- stoor
|
@@ -121,22 +154,25 @@ files:
|
|
121
154
|
- bin/stoor
|
122
155
|
- config.ru
|
123
156
|
- lib/stoor.rb
|
124
|
-
- lib/stoor/add_after.rb
|
125
|
-
- lib/stoor/decorate.rb
|
126
157
|
- lib/stoor/git_config.rb
|
127
158
|
- lib/stoor/github_auth.rb
|
128
159
|
- lib/stoor/logger.rb
|
160
|
+
- lib/stoor/transform_content.rb
|
129
161
|
- lib/stoor/version.rb
|
130
162
|
- lib/stoor/views/layout.rb
|
131
163
|
- lib/stoor/views/logout.mustache
|
132
164
|
- lib/stoor/views/logout.rb
|
133
165
|
- log/.gitignore
|
134
166
|
- public/.gitignore
|
135
|
-
- spec/lib/stoor/
|
167
|
+
- spec/lib/stoor/git_config_spec.rb
|
168
|
+
- spec/lib/stoor/github_auth_spec.rb
|
169
|
+
- spec/lib/stoor/transform_content_spec.rb
|
170
|
+
- spec/repo.rb
|
136
171
|
- spec/spec_helper.rb
|
137
172
|
- stoor.gemspec
|
138
|
-
homepage: https://
|
139
|
-
licenses:
|
173
|
+
homepage: https://github.com/jgn/stoor
|
174
|
+
licenses:
|
175
|
+
- MIT
|
140
176
|
post_install_message:
|
141
177
|
rdoc_options:
|
142
178
|
- --charset=UTF-8
|
@@ -161,5 +197,8 @@ signing_key:
|
|
161
197
|
specification_version: 3
|
162
198
|
summary: Front-end for Gollum
|
163
199
|
test_files:
|
164
|
-
- spec/lib/stoor/
|
200
|
+
- spec/lib/stoor/git_config_spec.rb
|
201
|
+
- spec/lib/stoor/github_auth_spec.rb
|
202
|
+
- spec/lib/stoor/transform_content_spec.rb
|
203
|
+
- spec/repo.rb
|
165
204
|
- spec/spec_helper.rb
|
data/lib/stoor/add_after.rb
DELETED
@@ -1,44 +0,0 @@
|
|
1
|
-
require 'rack'
|
2
|
-
|
3
|
-
module Stoor
|
4
|
-
class AddAfter
|
5
|
-
include Rack::Utils
|
6
|
-
|
7
|
-
attr_reader :status, :headers
|
8
|
-
attr_reader :rxp, :string, :content_type
|
9
|
-
|
10
|
-
def initialize(app, rxp, string, content_type = 'text/html')
|
11
|
-
@app, @rxp, @string, @content_type = app, rxp, string, content_type
|
12
|
-
end
|
13
|
-
|
14
|
-
def call(env)
|
15
|
-
@status, @headers, response = @app.call(env)
|
16
|
-
@headers = HeaderHash.new(@headers)
|
17
|
-
|
18
|
-
if has_body && not_encoded && headers['content-type'] &&
|
19
|
-
headers['content-type'].index(content_type)
|
20
|
-
|
21
|
-
content = Array(response).join('')
|
22
|
-
|
23
|
-
if content =~ rxp
|
24
|
-
pre, match, post = $`, $&, $'
|
25
|
-
new_body = pre + match + string + post
|
26
|
-
headers['Content-Length'] = new_body.bytesize.to_s
|
27
|
-
return [status, headers, [new_body]]
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
[status, headers, response]
|
32
|
-
end
|
33
|
-
|
34
|
-
private
|
35
|
-
|
36
|
-
def has_body
|
37
|
-
!STATUS_WITH_NO_ENTITY_BODY.include?(status)
|
38
|
-
end
|
39
|
-
|
40
|
-
def not_encoded
|
41
|
-
!headers['transfer-encoding']
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
data/lib/stoor/decorate.rb
DELETED
@@ -1,63 +0,0 @@
|
|
1
|
-
module Stoor
|
2
|
-
class Decorate
|
3
|
-
include Rack::Utils
|
4
|
-
|
5
|
-
FOOTER_REGEXP = /(<div id="footer">)(.*?)(<\/div>)/im
|
6
|
-
|
7
|
-
def initialize(app); @app = app; end
|
8
|
-
|
9
|
-
def call(env)
|
10
|
-
@request = Rack::Request.new(env)
|
11
|
-
|
12
|
-
if @request.session['gollum.author'].nil?
|
13
|
-
@request.logger.info "No 'gollum.author' in session - skipping page decoration."
|
14
|
-
return @app.call(env)
|
15
|
-
end
|
16
|
-
|
17
|
-
status, headers, response = @app.call(env)
|
18
|
-
headers = HeaderHash.new(headers)
|
19
|
-
|
20
|
-
if !STATUS_WITH_NO_ENTITY_BODY.include?(status) &&
|
21
|
-
!headers['transfer-encoding'] &&
|
22
|
-
headers['content-type'] &&
|
23
|
-
headers['content-type'].include?("text/html")
|
24
|
-
|
25
|
-
# TODO: If the response isn't an Array, it's a Rack::File or something, so ignore
|
26
|
-
if response.respond_to? :inject
|
27
|
-
content = response.inject("") { |content, part| content << part }
|
28
|
-
if match_data = content.match(FOOTER_REGEXP)
|
29
|
-
new_body = "" <<
|
30
|
-
match_data.pre_match <<
|
31
|
-
match_data[1] <<
|
32
|
-
before_existing_footer <<
|
33
|
-
match_data[2] <<
|
34
|
-
after_existing_footer <<
|
35
|
-
match_data[3] <<
|
36
|
-
match_data.post_match
|
37
|
-
headers['Content-Length'] = new_body.bytesize.to_s
|
38
|
-
return [status, headers, [new_body]]
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
[status, headers, response]
|
44
|
-
end
|
45
|
-
|
46
|
-
def before_existing_footer
|
47
|
-
<<-HTML
|
48
|
-
<div style="float: left;">
|
49
|
-
HTML
|
50
|
-
end
|
51
|
-
|
52
|
-
def after_existing_footer
|
53
|
-
<<-HTML
|
54
|
-
</div>
|
55
|
-
<div style="float: right;">
|
56
|
-
<p style="text-align: right; font-size: .9em; line-height: 1.6em; color: #999; margin: 0.9em 0;">
|
57
|
-
Commiting as <b>#{@request.session['gollum.author'][:name]}</b> (#{@request.session['gollum.author'][:email]})#{" | <a href='/logout'>Logout</a>" if ENV['GITHUB_AUTHORIZED']}
|
58
|
-
</p>
|
59
|
-
</div>
|
60
|
-
HTML
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
@@ -1,99 +0,0 @@
|
|
1
|
-
require 'stoor/add_after'
|
2
|
-
require 'spec_helper'
|
3
|
-
|
4
|
-
module Stoor
|
5
|
-
|
6
|
-
describe AddAfter, 'content-type matching' do
|
7
|
-
|
8
|
-
context 'default with text/plain response' do
|
9
|
-
let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/plain' }, '<body>'] } }
|
10
|
-
let(:app) { AddAfter.new(inner_app, /<body>/, 'stuff') }
|
11
|
-
|
12
|
-
it "skips" do
|
13
|
-
get '/'
|
14
|
-
expect(last_response.body).to eq('<body>')
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
context 'default with text/html response' do
|
19
|
-
let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/html' }, '<body>'] } }
|
20
|
-
let(:app) { AddAfter.new(inner_app, /<body>/, 'stuff') }
|
21
|
-
|
22
|
-
it "processes" do
|
23
|
-
get '/'
|
24
|
-
expect(last_response.body).to eq('<body>stuff')
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
context 'specifying text/plain with text/html response' do
|
29
|
-
let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/html' }, '<body>'] } }
|
30
|
-
let(:app) { AddAfter.new(inner_app, /<body>/, 'stuff', 'text/plain') }
|
31
|
-
|
32
|
-
it "skips" do
|
33
|
-
get '/'
|
34
|
-
expect(last_response.body).to eq('<body>')
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
context 'specifying regexp with text/html response' do
|
39
|
-
let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/whatever' }, '<body>'] } }
|
40
|
-
let(:app) { AddAfter.new(inner_app, /<body>/, 'stuff', /\Atext\//) }
|
41
|
-
|
42
|
-
it "processes" do
|
43
|
-
get '/'
|
44
|
-
expect(last_response.body).to eq('<body>stuff')
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
end
|
49
|
-
|
50
|
-
describe AddAfter, 'insertion' do
|
51
|
-
let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/html' }, '<body>'] } }
|
52
|
-
let(:app) { AddAfter.new(inner_app, /<body>/, 'stuff') }
|
53
|
-
|
54
|
-
it 'adds text after the first occurrence of a search string' do
|
55
|
-
get '/'
|
56
|
-
expect(last_response.body).to eq('<body>stuff')
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
describe AddAfter, 'insertion with two potential matches' do
|
61
|
-
let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/html' }, '<body> <body>'] } }
|
62
|
-
let(:app) { AddAfter.new(inner_app, /<body>/, 'stuff') }
|
63
|
-
|
64
|
-
it 'adds text only after the first occurrence of a search string' do
|
65
|
-
get '/'
|
66
|
-
expect(last_response.body).to eq('<body>stuff <body>')
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
describe AddAfter, 'insertion if response is an Array' do
|
71
|
-
let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/html' }, [ '<bo', 'dy>' ] ] } }
|
72
|
-
let(:app) { AddAfter.new(inner_app, /<body>/, 'stuff') }
|
73
|
-
|
74
|
-
it 'adds text after the first occurrence of a search string' do
|
75
|
-
get '/'
|
76
|
-
expect(last_response.body).to eq('<body>stuff')
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
describe AddAfter, 'status code implies no body' do
|
81
|
-
let(:inner_app) { ->(env) { [204, { 'Content-Type' => 'text/html' }, '<body>' ] } }
|
82
|
-
let(:app) { AddAfter.new(inner_app, /<body>/, 'stuff') }
|
83
|
-
|
84
|
-
it 'skips' do
|
85
|
-
get '/'
|
86
|
-
expect(last_response.body).to eq('<body>')
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
|
-
describe AddAfter, 'response is transfer-encoded' do
|
91
|
-
let(:inner_app) { ->(env) { [200, { 'Transfer-Encoding' => 'Chunked', 'Content-Type' => 'text/html' }, '<body>' ] } }
|
92
|
-
let(:app) { AddAfter.new(inner_app, /<body>/, 'stuff') }
|
93
|
-
|
94
|
-
it 'skips' do
|
95
|
-
get '/'
|
96
|
-
expect(last_response.body).to eq('<body>')
|
97
|
-
end
|
98
|
-
end
|
99
|
-
end
|