1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
require_relative "../test_helper"
require "rack/test"
require "tmpdir"
require "pro_tacts/unhandled_requests"
class UnhandledRequestsTest < Minitest::Test
include Rack::Test::Methods
# Returns whatever status the test asks for, so each case can drive the
# capture decision directly.
class Stub
attr_accessor :status
def initialize
@status = 200
end
def call(_env)
[@status, { "Content-Type" => "text/plain" }, ["body from the app"]]
end
end
def setup
@directory = Pathname.new(Dir.mktmpdir("unhandled"))
@stub = Stub.new
@app = ProTacts::UnhandledRequests.new(@stub, directory: @directory)
end
def teardown
FileUtils.remove_entry(@directory)
end
attr_reader :app
def captures
@directory.children.select(&:directory?)
end
def test_successful_requests_are_not_captured
@stub.status = 200
get "/dav/addressbook/"
assert_empty captures
end
def test_a_404_is_captured
@stub.status = 404
get "/dav/unknown/"
assert_equal 1, captures.size
end
def test_a_server_error_is_captured
@stub.status = 500
get "/dav/addressbook/"
assert_equal 1, captures.size
end
# An unsupported REPORT type. TailscaleAuth refuses unauthenticated
# requests above this middleware, so a 403 reaching it is always the app
# saying it routed the request and will not serve it.
def test_a_403_from_the_app_is_captured
@stub.status = 403
request "/dav/addressbook/", method: "REPORT", input: "<addressbook-query/>"
assert_equal 1, captures.size
end
def test_capture_records_the_request_verbatim
@stub.status = 404
request "/dav/addressbook/", method: "REPORT", input: "<sync-collection/>",
"CONTENT_TYPE" => "text/xml", "HTTP_DEPTH" => "1"
recorded = (captures.first / "request").read
assert_match(%r{\AREPORT /dav/addressbook/ HTTP/1\.\d\n}, recorded)
assert_includes recorded, "Depth: 1"
assert_includes recorded, "Content-Type: text/xml"
assert_includes recorded, "<sync-collection/>"
end
def test_capture_records_the_response
@stub.status = 404
get "/dav/unknown/"
recorded = (captures.first / "response").read
assert_includes recorded, "404"
assert_includes recorded, "Content-Type: text/plain"
assert_includes recorded, "body from the app"
end
# The whole point of the format: a capture drops into the fixture suite.
def test_capture_uses_the_fixture_layout
@stub.status = 404
get "/dav/unknown/"
assert_equal %w[request response], captures.first.children.map { it.basename.to_s }.sort
end
def test_the_app_still_sees_its_own_body
@stub.status = 404
get "/dav/unknown/"
assert_equal "body from the app", last_response.body
end
def test_a_repeated_request_is_captured_once
@stub.status = 404
3.times { get "/dav/unknown/" }
assert_equal 1, captures.size
end
def test_different_requests_are_captured_separately
@stub.status = 404
get "/dav/unknown/"
get "/dav/other/"
assert_equal 2, captures.size
end
# Same path, different body: the multiget case, where what was asked for
# is the part that matters.
def test_the_body_distinguishes_captures
@stub.status = 404
request "/dav/addressbook/", method: "REPORT", input: "<one/>"
request "/dav/addressbook/", method: "REPORT", input: "<two/>"
assert_equal 2, captures.size
end
def test_an_unwritable_directory_does_not_break_the_response
app = ProTacts::UnhandledRequests.new(@stub, directory: @directory / "nested" / "deep")
@stub.status = 404
session = Rack::Test::Session.new(Rack::MockSession.new(app))
session.get "/dav/unknown/"
assert_equal 404, session.last_response.status
end
end