1% Licensed under the Apache License, Version 2.0 (the "License"); you may not
2% use this file except in compliance with the License. You may obtain a copy of
3% the License at
4%
5%   http://www.apache.org/licenses/LICENSE-2.0
6%
7% Unless required by applicable law or agreed to in writing, software
8% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10% License for the specific language governing permissions and limitations under
11% the License.
12
13-module(couchdb_mrview_tests).
14
15-include_lib("couch/include/couch_eunit.hrl").
16-include_lib("couch/include/couch_db.hrl").
17
18
19
20-define(DDOC, {[
21    {<<"_id">>, <<"_design/foo">>},
22    {<<"shows">>, {[
23        {<<"bar">>, <<"function(doc, req) {return '<h1>wosh</h1>';}">>}
24    ]}},
25    {<<"updates">>, {[
26        {<<"report">>, <<"function(doc, req) {"
27            "var data = JSON.parse(req.body); "
28            "return ['test', data];"
29        "}">>}
30    ]}},
31    {<<"views">>, {[
32        {<<"view1">>, {[
33            {<<"map">>, <<"function(doc){emit(doc._id, doc._rev)}">>}
34        ]}}
35    ]}}
36]}).
37
38-define(USER, "admin").
39-define(PASS, "pass").
40-define(AUTH, {basic_auth, {?USER, ?PASS}}).
41
42
43setup_all() ->
44    Ctx = test_util:start_couch([chttpd]),
45    ok = meck:new(mochiweb_socket, [passthrough]),
46    Hashed = couch_passwords:hash_admin_password(?PASS),
47    ok = config:set("admins", ?USER, ?b2l(Hashed), _Persist=false),
48    Ctx.
49
50teardown_all(Ctx) ->
51    meck:unload(),
52    ok = config:delete("admins", ?USER, _Persist=false),
53    test_util:stop_couch(Ctx).
54
55setup(PortType) ->
56    meck:reset([mochiweb_socket]),
57    ok = meck:expect(mochiweb_socket, recv, fun mochiweb_socket_recv/3),
58
59    DbName = ?tempdb(),
60    ok = create_db(PortType, DbName),
61
62    Host = host_url(PortType),
63    upload_ddoc(Host, ?b2l(DbName)),
64    {Host, ?b2l(DbName)}.
65
66teardown(PortType, {_Host, DbName}) ->
67    delete_db(PortType, ?l2b(DbName)),
68    ok.
69
70mrview_show_test_() ->
71    {
72        "Check show functionality",
73        {
74            setup,
75            fun setup_all/0,
76            fun teardown_all/1,
77            [
78                make_test_case(clustered, [fun should_return_invalid_request_body/2]),
79                make_test_case(backdoor, [fun should_return_invalid_request_body/2])
80            ]
81        }
82    }.
83
84mrview_query_test_() ->
85    {
86        "Check view query functionality",
87        {
88            setup,
89            fun setup_all/0,
90            fun teardown_all/1,
91            [
92                make_test_case(clustered, [fun should_return_400_for_wrong_order_of_keys/2]),
93                make_test_case(backdoor, [fun should_return_400_for_wrong_order_of_keys/2])
94            ]
95        }
96    }.
97
98mrview_cleanup_index_files_test_() ->
99    {
100        "Check index files cleanup",
101        {
102            setup,
103            fun setup_all/0,
104            fun teardown_all/1,
105            [
106                make_test_case(clustered, [fun should_cleanup_index_files/2])
107            ]
108        }
109    }.
110
111
112make_test_case(Mod, Funs) ->
113    {
114        lists:flatten(io_lib:format("~s", [Mod])),
115        {
116            foreachx,
117            fun setup/1,
118            fun teardown/2,
119            [{Mod, Fun} || Fun <- Funs]
120        }
121    }.
122
123should_return_invalid_request_body(PortType, {Host, DbName}) ->
124    ?_test(begin
125         ok = create_doc(PortType, ?l2b(DbName), <<"doc_id">>, {[]}),
126         ReqUrl = Host ++ "/" ++ DbName ++ "/_design/foo/_update/report/doc_id",
127         {ok, Status, _Headers, Body} =
128              test_request:post(ReqUrl, [?AUTH], <<"{truncated}">>),
129         {Props} = jiffy:decode(Body),
130         ?assertEqual(
131            <<"bad_request">>, couch_util:get_value(<<"error">>, Props)),
132         ?assertEqual(
133            <<"Invalid request body">>, couch_util:get_value(<<"reason">>, Props)),
134         ?assertEqual(400, Status),
135         ok
136    end).
137
138should_return_400_for_wrong_order_of_keys(_PortType, {Host, DbName}) ->
139    Args = [{start_key, "\"bbb\""}, {end_key, "\"aaa\""}],
140    ?_test(begin
141         ReqUrl = Host ++ "/" ++ DbName
142              ++ "/_design/foo/_view/view1?" ++ mochiweb_util:urlencode(Args),
143         {ok, Status, _Headers, Body} = test_request:get(ReqUrl, [?AUTH]),
144         {Props} = jiffy:decode(Body),
145         ?assertEqual(
146            <<"query_parse_error">>, couch_util:get_value(<<"error">>, Props)),
147         ?assertEqual(
148            <<"No rows can match your key range, reverse your start_key and end_key or set descending=true">>,
149            couch_util:get_value(<<"reason">>, Props)),
150         ?assertEqual(400, Status),
151         ok
152    end).
153
154should_cleanup_index_files(_PortType, {Host, DbName}) ->
155    ?_test(begin
156        IndexWildCard = [
157            config:get("couchdb", "view_index_dir"),
158            "/.shards/*/",
159            DbName,
160            ".[0-9]*_design/mrview/*"
161        ],
162        ReqUrl = Host ++ "/" ++ DbName ++ "/_design/foo/_view/view1",
163        {ok, _Status0, _Headers0, _Body0} = test_request:get(ReqUrl, [?AUTH]),
164        FileList0 = filelib:wildcard(IndexWildCard),
165        ?assertNotEqual([], FileList0),
166
167        % It is hard to simulate inactive view.
168        % Since couch_mrview:cleanup is called on view definition change.
169        % That's why we just create extra files in place
170        ToDelete = lists:map(fun(FilePath) ->
171            ViewFile = filename:join([
172                filename:dirname(FilePath),
173                "11111111111111111111111111111111.view"]),
174            file:write_file(ViewFile, <<>>),
175            ViewFile
176        end, FileList0),
177        FileList1 = filelib:wildcard(IndexWildCard),
178        ?assertEqual([], lists:usort(FileList1 -- (FileList0 ++ ToDelete))),
179
180        CleanupUrl = Host ++ "/" ++ DbName ++ "/_view_cleanup",
181        {ok, _Status1, _Headers1, _Body1} = test_request:post(
182            CleanupUrl, [], <<>>, [?AUTH]),
183        test_util:wait(fun() ->
184                IndexFiles = filelib:wildcard(IndexWildCard),
185                case lists:usort(FileList0) == lists:usort(IndexFiles) of
186                    false -> wait;
187                    true -> ok
188                end
189        end),
190        ok
191    end).
192
193
194create_doc(backdoor, DbName, Id, Body) ->
195    JsonDoc = couch_util:json_apply_field({<<"_id">>, Id}, Body),
196    Doc = couch_doc:from_json_obj(JsonDoc),
197    {ok, Db} = couch_db:open(DbName, [?ADMIN_CTX]),
198    {ok, _} = couch_db:update_docs(Db, [Doc]),
199    couch_db:close(Db);
200create_doc(clustered, DbName, Id, Body) ->
201    JsonDoc = couch_util:json_apply_field({<<"_id">>, Id}, Body),
202    Doc = couch_doc:from_json_obj(JsonDoc),
203    {ok, _} = fabric:update_docs(DbName, [Doc], [?ADMIN_CTX]),
204    ok.
205
206create_db(backdoor, DbName) ->
207    {ok, Db} = couch_db:create(DbName, [?ADMIN_CTX]),
208    couch_db:close(Db);
209create_db(clustered, DbName) ->
210    {ok, Status, _, _} = test_request:put(db_url(DbName), [?AUTH], ""),
211    assert_success(create_db, Status),
212    ok.
213
214delete_db(backdoor, DbName) ->
215    couch_server:delete(DbName, [?ADMIN_CTX]);
216delete_db(clustered, DbName) ->
217    {ok, Status, _, _} = test_request:delete(db_url(DbName), [?AUTH]),
218    assert_success(delete_db, Status),
219    ok.
220
221assert_success(create_db, Status) ->
222    ?assert(lists:member(Status, [201, 202]));
223assert_success(delete_db, Status) ->
224    ?assert(lists:member(Status, [200, 202])).
225
226
227host_url(PortType) ->
228    "http://" ++ bind_address(PortType) ++ ":" ++ port(PortType).
229
230bind_address(PortType) ->
231    config:get(section(PortType), "bind_address", "127.0.0.1").
232
233section(backdoor) -> "http";
234section(clustered) -> "chttpd".
235
236db_url(DbName) when is_binary(DbName) ->
237    db_url(binary_to_list(DbName));
238db_url(DbName) when is_list(DbName) ->
239    host_url(clustered) ++ "/" ++ DbName.
240
241port(clustered) ->
242    integer_to_list(mochiweb_socket_server:get(chttpd, port));
243port(backdoor) ->
244    integer_to_list(mochiweb_socket_server:get(couch_httpd, port)).
245
246
247upload_ddoc(Host, DbName) ->
248    Url = Host ++ "/" ++ DbName ++ "/_design/foo",
249    Body = couch_util:json_encode(?DDOC),
250    {ok, 201, _Resp, _Body} = test_request:put(Url, [?AUTH], Body),
251    ok.
252
253mochiweb_socket_recv(Sock, Len, Timeout) ->
254    case meck:passthrough([Sock, Len, Timeout]) of
255        {ok, <<"{truncated}">>} ->
256            {error, closed};
257        {ok, Data} ->
258            {ok, Data};
259        Else ->
260            Else
261    end.
262