stoor 0.1.4 → 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|