diff --git a/tests/ts_simple_http_api_SUITE.erl b/tests/ts_simple_http_api_SUITE.erl new file mode 100644 index 000000000..9c42198b2 --- /dev/null +++ b/tests/ts_simple_http_api_SUITE.erl @@ -0,0 +1,368 @@ +%% ------------------------------------------------------------------- +%% +%% Copyright (c) 2016 Basho Technologies, Inc. +%% +%% This file is provided to you under the Apache License, +%% Version 2.0 (the "License"); you may not use this file +%% except in compliance with the License. You may obtain +%% a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, +%% software distributed under the License is distributed on an +%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +%% KIND, either express or implied. See the License for the +%% specific language governing permissions and limitations +%% under the License. +%% +%% Tests for range queries around the boundaries of quanta. +%% +%% ------------------------------------------------------------------- +-module(ts_simple_http_api_SUITE). + +-compile(export_all). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +%%-------------------------------------------------------------------- +%% COMMON TEST CALLBACK FUNCTIONS +%%-------------------------------------------------------------------- + +suite() -> + [{timetrap,{minutes,10}}]. + +init_per_suite(Config) -> + [_Node|_] = Cluster = ts_util:build_cluster(single), + rt:wait_until_nodes_ready(Cluster), + [{cluster, Cluster} | Config]. + +end_per_suite(_Config) -> + ok. + +init_per_group(_GroupName, Config) -> + Config. + +end_per_group(_GroupName, _Config) -> + ok. + +init_per_testcase(_TestCase, Config) -> + Config. + +end_per_testcase(_TestCase, _Config) -> + ok. + +groups() -> + []. + +all() -> + [ create_table_test, + create_bad_table_test, + create_existing_table_test, + describe_table_test, + describe_nonexisting_table_test, + bad_describe_query_test, + post_single_row_test, + post_single_row_missing_field_test, + post_single_row_wrong_field_test, + post_several_rows_test, + post_row_to_nonexisting_table_test, + list_keys_test, + list_keys_nonexisting_table_test, + select_test, + select_subset_test, + invalid_select_test, + invalid_query_test, + delete_data_existing_row_test, + delete_data_nonexisting_row_test, + delete_data_nonexisting_table_test, + delete_data_wrong_path_test + ]. + + +%% column_names_def_1() -> +%% [<<"a">>, <<"b">>, <<"c">>]. + + +table_def_bob() -> + "create table bob (" + " a varchar not null," + " b varchar not null," + " c timestamp not null," + " d sint64," + " primary key ((a, b, quantum(c, 1, m)), a, b, c))". + +bad_table_def() -> + "create table pap (" + " a timestamp not null," + " b timestamp not null," + " c timestamp not null)". + +%% client_pid(Ctx) -> +%% [Node|_] = proplists:get_value(cluster, Ctx), +%% rt:pbc(Node). + +%%% +%%% HTTP API tests +%%% + +%%% query +create_table_test(Cfg) -> + Query = table_def_bob(), + {ok, "200", _Headers, Body } = execute_query(Query, Cfg), + Body = success_body(). + +create_bad_table_test(Cfg) -> + Query = bad_table_def(), + {ok, "400", Headers, Body} = execute_query(Query, Cfg), + "text/plain" = content_type(Headers), + "Query error: Missing primary key" = Body. + + +create_existing_table_test(Cfg) -> + Query = table_def_bob(), + {ok, "409", Headers, Body} = + execute_query(Query, Cfg), + "text/plain" = content_type(Headers), + "Table \"bob\" already exists" = Body. + + +describe_table_test(Cfg) -> + Query = "describe bob", + {ok, "200", Headers, Body } = execute_query(Query, Cfg), + "application/json" = content_type(Headers), + "{\"columns\":"++_ = Body. + +describe_nonexisting_table_test(Cfg) -> + Query = "describe john", + {ok, "404", Headers, Body} = execute_query(Query, Cfg), + "text/plain" = content_type(Headers), + "Table \"john\" does not exist" = Body. + +bad_describe_query_test(Cfg) -> + Query = "descripe bob", + {ok, "400", Headers, Body} = execute_query(Query, Cfg), + "text/plain" = content_type(Headers), + "Query error: Unexpected token: 'descripe'" = Body. + +%%% put +post_single_row_test(Cfg) -> + RowStr = row("q1", "w1", 11, 110), + {ok, "200", Headers, RespBody} = post_data("bob", RowStr, Cfg), + "application/json" = content_type(Headers), + RespBody = success_body(). + +post_single_row_missing_field_test(Cfg) -> + RowStr = missing_field_row("q1", 12, 200), + {ok, "400", Headers, Body} = + post_data("bob", RowStr, Cfg), + "text/plain" = content_type(Headers), + "Missing field \"b\" for key in table \"bob\"" = Body. + +post_single_row_wrong_field_test(Cfg) -> + RowStr = wrong_field_type_row("q1", "w1", 12, "raining"), + {ok,"400", Headers, Body} = post_data("bob", RowStr, Cfg), + "text/plain" = content_type(Headers), + "Bad value for field \"d\" of type sint64 in table \"bob\"" = Body. + + +post_several_rows_test(Cfg) -> + RowStrs = string:join([row("q1", "w2", 20, 150), row("q1", "w1", 20, 119)], + ", "), + Body = io_lib:format("[~s]", [RowStrs]), + {ok, "200", Headers, RespBody} = post_data("bob", Body, Cfg), + "application/json" = content_type(Headers), + RespBody = success_body(). + +post_row_to_nonexisting_table_test(Cfg) -> + RowStr = row("q1", "w1", 30, 142), + {ok,"404", Headers, Body} = post_data("bill", RowStr, Cfg), + "text/plain" = content_type(Headers), + "Table \"bill\" does not exist" = Body. + +%%% list_keys +list_keys_test(Cfg) -> + {"200", Headers, Body} = list_keys("bob", Cfg), + "text/plain" = content_type(Headers), + RecordURLs = string:tokens(Body, "\n"), + ?assertEqual(length(RecordURLs), 3), + %% do a get on each key + lists:foreach( + fun(URL) -> + {ok, "200", _Headers, _Body} = ibrowse:send_req(URL, [], get) + end, + RecordURLs). + + +list_keys_nonexisting_table_test(Cfg) -> + {"404", Headers, Body} = list_keys("john", Cfg), + "text/plain" = content_type(Headers), + "Table \"john\" does not exist" = Body. + +%%% select +select_test(Cfg) -> + Select = "select * from bob where a='q1' and b='w1' and c>1 and c<99", + {ok,"200", Headers, Body} = execute_query(Select, Cfg), + "application/json" = content_type(Headers), + "{\"columns\":[\"a\",\"b\",\"c\",\"d\"]," + "\"rows\":[[\"q1\",\"w1\",11,110]," + "[\"q1\",\"w1\",20,119]]}" = Body. + +select_subset_test(Cfg) -> + Select = "select * from bob where a='q1' and b='w1' and c>1 and c<15", + {ok, "200", Headers, Body} = execute_query(Select, Cfg), + "application/json" = content_type(Headers), + "{\"columns\":[\"a\",\"b\",\"c\",\"d\"]," + "\"rows\":[[\"q1\",\"w1\",11,110]]}" = Body. + +invalid_select_test(Cfg) -> + Select = "select * from bob where a='q1' and c>1 and c<15", + %% @todo: this really ought to be a 4XX error, but digging into the errors + %% from riak_ql might be too much for this API. + {ok, "500", Headers, Body} = execute_query(Select, Cfg), + "text/plain" = content_type(Headers), + "Execution of select query failed on table \"bob\" (The 'b' parameter is part the primary key but not specified in the where clause.)" + = Body. + +invalid_query_test(Cfg) -> + Select = "OHNOES A DANGLING QUOTE ' ", + {ok, "400", Headers, Body} = execute_query(Select, Cfg), + "text/plain" = content_type(Headers), + "Query error: Unexpected token '''." = Body. + +%%% delete +delete_data_existing_row_test(Cfg) -> + {ok, "200", Headers, Body} = delete("bob", "q1", "w1", 11, Cfg), + "application/json" = content_type(Headers), + Body = success_body(), + Select = "select * from bob where a='q1' and b='w1' and c>1 and c<99", + {ok, "200", _Headers2, + "{\"columns\":[\"a\",\"b\",\"c\",\"d\"],\"rows\":[[\"q1\",\"w1\",20,119]]}"} = + execute_query(Select, Cfg). + +delete_data_nonexisting_row_test(Cfg) -> + {ok, "404", Headers, Body } = delete("bob", "q1", "w1", 500, Cfg), + "text/plain" = content_type(Headers), + "Key not found" + = Body. + +delete_data_nonexisting_table_test(Cfg) -> + {ok, "404", Headers, Body } = delete("bill", "q1", "w1", 20, Cfg), + "text/plain" = content_type(Headers), + "Table \"bill\" does not exist" = Body. + +delete_data_wrong_path_test(Cfg) -> + {ok, "400", Headers, Body} = delete_wrong_path("bob", "q1", "w1", 20, Cfg), + "text/plain" = content_type(Headers), + "Not all key-constituent fields given on URL" = Body. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% Helper functions +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +execute_query(Query, Cfg) -> + Node = get_node(Cfg), + URL = query_url(Node), + ibrowse:send_req(URL, [], post, Query). + +post_data(Table, Body, Cfg) -> + Node = get_node(Cfg), + URL = post_data_url(Node, Table), + ibrowse:send_req(URL, [{"Content-Type", "application/json"}], post, lists:flatten(Body)). + + +get_node(Cfg) -> + [Node|_] = ?config(cluster, Cfg), + Node. + +node_ip_and_port(Node) -> + {ok, [{IP, Port}]} = rpc:call(Node, application, get_env, [riak_api, http]), + {IP, Port}. + +query_url(Node) -> + {IP, Port} = node_ip_and_port(Node), + query_url(IP, Port). + +query_url(IP, Port) -> + lists:flatten( + io_lib:format("http://~s:~B/ts/v1/query", + [IP, Port])). + +post_data_url(Node, Table) -> + {IP, Port} = node_ip_and_port(Node), + lists:flatten( + io_lib:format("http://~s:~B/ts/v1/tables/~s/keys", + [IP, Port, Table])). + +list_keys(Table, Cfg) -> + Node = get_node(Cfg), + URL = list_keys_url(Node, Table), + {ibrowse_req_id, ReqID} = ibrowse:send_req(URL, [], get, [], [{stream_to, self()}]), + collect_stream(ReqID). + +collect_stream(ReqID) -> + {Code, Headers} = collect_headers(ReqID), + Body = collect_body(ReqID), + {Code, Headers, Body}. + +collect_headers(ReqID) -> + receive + {ibrowse_async_headers, ReqID, Code, Headers} -> + {Code, Headers} + end. + +collect_body(ReqID) -> + receive + {ibrowse_async_response, ReqID, BodyPart} -> + BodyPart ++ collect_body(ReqID); + {ibrowse_async_response_end, ReqID} -> + [] + end. + +list_keys_url(Node, Table) -> + {IP, Port} = node_ip_and_port(Node), + lists:flatten( + io_lib:format("http://~s:~B/ts/v1/tables/~s/list_keys", + [IP, Port, Table])). + +delete(Table, A, B, C, Cfg) -> + Node = get_node(Cfg), + URL = delete_url(Node, Table, A, B, C), + ibrowse:send_req(URL, [], delete). + +delete_url(Node, Table, A, B, C) -> + {IP, Port} = node_ip_and_port(Node), + lists:flatten( + io_lib:format("http://~s:~B/ts/v1/tables/~s/keys/a/~s/b/~s/c/~B", + [IP, Port, Table, A, B, C])). + +delete_wrong_path(Table, A, B, C, Cfg) -> + Node = get_node(Cfg), + URL = delete_url_wrong_path(Node, Table, A, B, C), + ibrowse:send_req(URL, [], delete). + +delete_url_wrong_path(Node, Table, A, B, C) -> + {IP, Port} = node_ip_and_port(Node), + lists:flatten( + io_lib:format("http://~s:~B/ts/v1/tables/~s/keys/a/~s/b/~s/d/~B", + [IP, Port, Table, A, B, C])). + + +row(A, B, C, D) -> + io_lib:format("{\"a\": \"~s\", \"b\": \"~s\", \"c\": ~B, \"d\":~B}", + [A, B, C, D]). + +missing_field_row(A, C, D) -> + io_lib:format("{\"a\": \"~s\", \"c\": ~B, \"d\":~B}", + [A, C, D]). + +wrong_field_type_row(A, B, C, D) -> + io_lib:format("{\"a\": \"~s\", \"b\": \"~s\", \"c\": ~B, \"d\":~p}", + [A, B, C, D]). + + +success_body() -> + "{\"success\":true}". + +content_type(Headers) -> + proplists:get_value("Content-Type", Headers).