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