From 58d8d5219411afb80587587fafee1eb0f612019e Mon Sep 17 00:00:00 2001 From: sjdonado Date: Sun, 16 Mar 2025 18:54:20 +0100 Subject: [PATCH] test: update cursor-based pagination test cases --- spec/integration/link_spec.cr | 160 ++++++++++++++++++++++++++++++---- spec/integration/ping_spec.cr | 2 +- spec/spec_helper.cr | 20 +++++ 3 files changed, 166 insertions(+), 16 deletions(-) diff --git a/spec/integration/link_spec.cr b/spec/integration/link_spec.cr index 0126998..aeaef60 100644 --- a/spec/integration/link_spec.cr +++ b/spec/integration/link_spec.cr @@ -97,7 +97,7 @@ describe "App::Controllers::Link" do }) response.headers["Location"].should eq(link) - response.headers["X-Forwarded-User-Agent"].should eq(user_agent) + response.headers["User-Agent"].should eq(user_agent) response.headers.has_key?("X-Forwarded-For").should be_true end @@ -161,8 +161,8 @@ describe "App::Controllers::Link" do end describe "All" do - it "should return all links" do - links = ["https://google.com", "google.com", "google.com.co"] + it "should return all links with pagination" do + links = ["https://sjdonado.com", "sjdonado.com", "sjdonado.com.co"] test_user = create_test_user() links.each do |link| @@ -171,14 +171,58 @@ describe "App::Controllers::Link" do get("/api/links", headers: HTTP::Headers{"X-Api-Key" => test_user.api_key.to_s}) - parsed_response = Hash(String, Array(Hash(String, String | Int64 | Array(Hash(String, String))))).from_json(response.body) - parsed_response["data"][0]["origin"].should eq(links[0]) - parsed_response["data"][1]["origin"].should eq(links[1]) - parsed_response["data"][2]["origin"].should eq(links[2]) + parsed_response = Hash(String, Array(Hash(String, String | Int64)) | Hash(String, Bool | String?)).from_json(response.body) + + # Check that each link is in the response data + origins = parsed_response["data"].as(Array).map { |link| link["origin"] } + links.each do |link| + origins.should contain(link) + end + + parsed_response["pagination"].as(Hash)["has_more"].should be_false + end + + it "should respect custom limit parameter" do + test_user = create_test_user() + + 5.times do |i| + create_test_link(test_user, "https://example.com/#{i}") + end + + get("/api/links?limit=2", headers: HTTP::Headers{"X-Api-Key" => test_user.api_key.to_s}) + + parsed_response = Hash(String, Array(Hash(String, String | Int64)) | Hash(String, Bool | String?)).from_json(response.body) + parsed_response["data"].as(Array).size.should eq(2) + parsed_response["pagination"].as(Hash)["has_more"].should be_true + parsed_response["pagination"].as(Hash)["next"].should_not be_nil + end + + it "should support cursor-based pagination" do + test_user = create_test_user() + + 5.times do |i| + create_test_link(test_user, "https://example.com/#{i}") + end + + # Get first page + get("/api/links?limit=2", headers: HTTP::Headers{"X-Api-Key" => test_user.api_key.to_s}) + first_page = Hash(String, Array(Hash(String, String | Int64)) | Hash(String, Bool | String?)).from_json(response.body) + cursor = first_page["pagination"].as(Hash)["next"] + + # Get second page using cursor + get("/api/links?limit=2&cursor=#{cursor}", headers: HTTP::Headers{"X-Api-Key" => test_user.api_key.to_s}) + second_page = Hash(String, Array(Hash(String, String | Int64)) | Hash(String, Bool | String?)).from_json(response.body) + + # Ensure different links are returned + first_page_ids = first_page["data"].as(Array).map { |link| link["id"] } + second_page_ids = second_page["data"].as(Array).map { |link| link["id"] } + + # Check that no IDs from first page appear in second page + (first_page_ids & second_page_ids).empty?.should be_true end it "should return owned links only" do - links = ["https://google.de", "google.de", "google.edu.co", "x.com"] + links = ["https://donado.co", "donado.co", "uninorte.edu.co", "kagi.com"] test_user = create_test_user() links[0..2].each do |link| @@ -190,11 +234,14 @@ describe "App::Controllers::Link" do get("/api/links", headers: HTTP::Headers{"X-Api-Key" => test_user.api_key.to_s}) - parsed_response = Hash(String, Array(Hash(String, String | Int64 | Array(Hash(String, String))))).from_json(response.body) - parsed_response["data"].size.should eq(3) - parsed_response["data"][0]["origin"].should eq(links[0]) - parsed_response["data"][1]["origin"].should eq(links[1]) - parsed_response["data"][2]["origin"].should eq(links[2]) + parsed_response = Hash(String, Array(Hash(String, String | Int64)) | Hash(String, Bool | String?)).from_json(response.body) + parsed_response["data"].as(Array).size.should eq(3) + + origins = parsed_response["data"].as(Array).map { |link| link["origin"] } + links[0..2].each do |link| + origins.should contain(link) + end + origins.should_not contain(links[3]) end it "should return 401 - missing api key" do @@ -207,16 +254,20 @@ describe "App::Controllers::Link" do end describe "Get" do - it "should return the specified link with click details" do + it "should return the specified link with limited click details" do link = "https://bing.com" test_user = create_test_user() test_link = create_test_link(test_user, link) + 110.times do + create_test_click(test_link) + end + get("/api/links/#{test_link.id}", headers: HTTP::Headers{"X-Api-Key" => test_user.api_key.to_s}) parsed_response = Hash(String, Hash(String, String | Int64 | Array(Hash(String, String)))).from_json(response.body) parsed_response["data"]["origin"].should eq(link) - parsed_response["data"]["clicks"].should be_a(Array(Hash(String, String))) + parsed_response["data"]["clicks"].as(Array).size.should eq(100) end it "should return 404 - link does not exist" do @@ -238,6 +289,85 @@ describe "App::Controllers::Link" do end end + describe "Clicks" do + it "should return paginated clicks for a link" do + link = "https://example.com" + test_user = create_test_user() + test_link = create_test_link(test_user, link) + + 5.times do + create_test_click(test_link) + end + + get("/api/links/#{test_link.id}/clicks", headers: HTTP::Headers{"X-Api-Key" => test_user.api_key.to_s}) + + parsed_response = Hash(String, Array(Hash(String, String)) | Hash(String, Bool | String?)).from_json(response.body) + parsed_response["data"].as(Array).size.should eq(5) + parsed_response["pagination"].as(Hash)["has_more"].should be_false + end + + it "should respect limit parameter" do + link = "https://example.com" + test_user = create_test_user() + test_link = create_test_link(test_user, link) + + 10.times do + create_test_click(test_link) + end + + get("/api/links/#{test_link.id}/clicks?limit=3", headers: HTTP::Headers{"X-Api-Key" => test_user.api_key.to_s}) + + parsed_response = Hash(String, Array(Hash(String, String)) | Hash(String, Bool | String?)).from_json(response.body) + parsed_response["data"].as(Array).size.should eq(3) + parsed_response["pagination"].as(Hash)["has_more"].should be_true + parsed_response["pagination"].as(Hash)["next"].should_not be_nil + end + + it "should support cursor-based pagination" do + link = "https://example.com" + test_user = create_test_user() + test_link = create_test_link(test_user, link) + + 10.times do + create_test_click(test_link) + end + + # Get first page + get("/api/links/#{test_link.id}/clicks?limit=3", headers: HTTP::Headers{"X-Api-Key" => test_user.api_key.to_s}) + first_page = Hash(String, Array(Hash(String, String)) | Hash(String, Bool | String?)).from_json(response.body) + cursor = first_page["pagination"].as(Hash)["next"] + + # Get second page using cursor + get("/api/links/#{test_link.id}/clicks?limit=3&cursor=#{cursor}", headers: HTTP::Headers{"X-Api-Key" => test_user.api_key.to_s}) + second_page = Hash(String, Array(Hash(String, String)) | Hash(String, Bool | String?)).from_json(response.body) + + # Ensure different clicks are returned + first_page_ids = first_page["data"].as(Array).map { |click| click["id"] } + second_page_ids = second_page["data"].as(Array).map { |click| click["id"] } + + # Check that no IDs from first page appear in second page + (first_page_ids & second_page_ids).empty?.should be_true + end + + it "should return 404 - link does not exist" do + test_user = create_test_user() + + get("/api/links/nonexistent_id/clicks", headers: HTTP::Headers{"X-Api-Key" => test_user.api_key.to_s}) + + expected = {"error" => "Resource not found"}.to_json + response.status_code.should eq(404) + response.body.should eq(expected) + end + + it "should return 401 - missing api key" do + get("/api/links/1/clicks") + + expected = {"error" => "Unauthorized access"}.to_json + response.status_code.should eq(401) + response.body.should eq(expected) + end + end + describe "Update" do it "should update link url" do link = "https://github.com" diff --git a/spec/integration/ping_spec.cr b/spec/integration/ping_spec.cr index c3434f1..5160072 100644 --- a/spec/integration/ping_spec.cr +++ b/spec/integration/ping_spec.cr @@ -4,7 +4,7 @@ describe "App::Controllers::Ping" do it "should return pong" do get "/api/ping" - expected = {"pong" => "ok"}.to_json + expected = {"data" => "pong"}.to_json response.body.should eq(expected) end end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 961ec4d..236ef98 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -55,6 +55,26 @@ def create_test_link(user, url) link end +def create_test_click(link) + click = App::Models::Click.new + click.id = UUID.v4.to_s + click.user_agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:127.0) Gecko/20100101 Firefox/127.0" + click.browser = "Firefox" + click.os = "Mac OS X" + click.referer = "example.com" + click.country = "US" + click.created_at = Time.utc + click.link = link + click.link_id = link.id + + changeset = App::Lib::Database.insert(click) + unless changeset.valid? + error_messages = changeset.errors.map { |error| "#{error}" }.join(", ") + raise "Test click creation failed: #{error_messages}" + end + click +end + def get_test_link(link_id) query = App::Lib::Database::Query.where(id: link_id.as(String)).limit(1) link = App::Lib::Database.all(App::Models::Link, query, preload: [:clicks]).first?