summary refs log tree commit diff homepage
path: root/spec
diff options
context:
space:
mode:
authorNguyễn Gia Phong <mcsinyx@disroot.org>2023-03-07 02:29:15 +0900
committerNguyễn Gia Phong <mcsinyx@disroot.org>2023-03-07 02:36:25 +0900
commitd378edca8215080fb0a86899f6dc52643bdb0852 (patch)
treedf84cf42e878805a61087b8b3cb9187e1eee60cd /spec
parent72e56c93dfda955dd2af05607c7afe2b510fee23 (diff)
downloadhybring-d378edca8215080fb0a86899f6dc52643bdb0852.tar.gz
Add integration tests for HTTP API
Diffstat (limited to 'spec')
-rw-r--r--spec/helper.cr29
-rw-r--r--spec/server.cr186
2 files changed, 215 insertions, 0 deletions
diff --git a/spec/helper.cr b/spec/helper.cr
new file mode 100644
index 0000000..6fe13e5
--- /dev/null
+++ b/spec/helper.cr
@@ -0,0 +1,29 @@
+# Spec helper
+# Copyright (C) 2023  Nguyễn Gia Phong
+#
+# This file if part of hybring.
+#
+# Hybring is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Hybring is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with hybring.  If not, see <https://www.gnu.org/licenses/>.
+
+require "spec"
+::SPEC = true
+
+require "../src/cli"
+
+CONFIG = Configuration.new({"general" => {"db" => File.tempname,
+                                          "api" => "/"},
+                            "opennic" => {"local" => File.tempname,
+                                          "remote" => "http://example.null"},
+                            "icann" => {"local" => File.tempname,
+                                          "remote" => "http://example.net"}})
diff --git a/spec/server.cr b/spec/server.cr
new file mode 100644
index 0000000..69bb543
--- /dev/null
+++ b/spec/server.cr
@@ -0,0 +1,186 @@
+# HTTP server spec
+# Copyright (C) 2023  Nguyễn Gia Phong
+#
+# This file if part of hybring.
+#
+# Hybring is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Hybring is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with hybring.  If not, see <https://www.gnu.org/licenses/>.
+
+require "file_utils"
+require "http/client"
+
+require "./helper"
+require "../src/http"
+require "../src/sqlite"
+
+BASE_DATA = {"nick" => "example",
+             "opennic" => "http://example.indy",
+             "icann" => "https://example.org",
+             "host" => "example.net"}
+
+def expect_errors(url, overlay, expected)
+  data = BASE_DATA.merge overlay
+  response = HTTP::Client.post url, form: URI::Params.encode data
+  response.status_code.should eq 400
+  response.content_type.should eq "application/xhtml+xml"
+  xml = XML.parse response.body
+  errors = {} of String => String
+  xml.xpath_nodes("//xhtml:form/xhtml:label[@class='error'][.!='Error:']",
+                  {"xhtml" => "http://www.w3.org/1999/xhtml"}).each do |node|
+    errors[node["for"]] = node.content
+  end
+  errors.should eq expected
+end
+
+url = uninitialized String
+Spec.before_suite do
+  server = Server.new CONFIG
+  spawn do
+    server.listen do |address|
+      url = "http://#{address}"
+    end
+  end
+  until server.listening?
+    sleep 1.milliseconds
+  end
+  Spec.after_suite do
+    server.close
+    FileUtils.rm_r [CONFIG.db, CONFIG.opennic_local, CONFIG.icann_local]
+  end
+end
+
+describe Server do
+  it "only accepts POST requests" do
+    %w(GET PUT HEAD DELETE PATCH OPTIONS).each do |method|
+      response = HTTP::Client.exec method, url
+      response.status_code.should eq 405
+    end
+  end
+
+  pending "requires Content-Length header" do
+    # FIXME: HTTP::Client automatically sets the header based on body
+  end
+
+  it "rejects too long content" do
+    response = HTTP::Client.post url, body: "x" * (MAX_CONTENT_LENGTH + 1)
+    response.status_code.should eq 413
+  end
+
+  it "only accepts application/x-www-form-urlencoded Content-Type" do
+    response = HTTP::Client.post url
+    response.status_code.should eq 415
+    %w(multipart/form-data text/html text/plain).each do |content_type|
+      headers = HTTP::Headers{"Content-Type" => content_type}
+      response = HTTP::Client.post url, headers
+      response.status_code.should eq 415
+    end
+  end
+
+  it "only allows parameters nick, opennic, icann or host" do
+    data = BASE_DATA.merge Hash{"foo" => "bar"}
+    response = HTTP::Client.post url, form: URI::Params.encode data
+    response.status_code.should eq 400
+    response.content_type.should eq "text/plain"
+    response.body.should eq "400 Invalid Parameter\n"
+  end
+
+  it "requires parameters nick, opennic, icann and host" do
+    k, keys = BASE_DATA.size, BASE_DATA.keys
+    until k = 0
+      keys.combinations(k).each do |c|
+        data = BASE_DATA.reject c
+        response = HTTP::Client.post url, form: URI::Params.encode data
+        response.status_code.should eq 400
+        response.content_type.should eq "text/plain"
+        response.body.should eq "400 Missing Parameter\n"
+      end
+      k -= 1
+    end
+  end
+
+  it "rejects too long nick" do
+    msg = "Must be within #{MAX_NICK_LENGTH} characters"
+    expect_errors url, {"nick" => "x" * (MAX_NICK_LENGTH + 1)}, {"nick" => msg}
+  end
+
+  it "rejects nicks that are not lowercase alphanumeric" do
+    %w(camelCase pun/tu?a#tion).each do |nick|
+      expect_errors url, {"nick" => nick},
+                    {"nick" => "Must be ASCII lowercase alphanumeric"}
+    end
+  end
+
+  it "rejects relative URLs" do
+    %w(foo /bar ex.am/ple).each do |uri|
+      expect_errors url, {"opennic" => uri, "icann" => uri},
+                    {"opennic" => "Must be absolute URL",
+                     "icann" => "Must be absolute URL"}
+    end
+  end
+
+  it "only accepts HTTP/S" do
+    expect_errors url, {"opennic" => "gopher://example.indy"},
+                  {"opennic" => "Must be HTTP/S"}
+    expect_errors url, {"opennic" => "https://example.indy",
+                        "icann" => "http://example.org"},
+                  {"icann" => "Must be HTTPS"}
+  end
+
+  it "checks for OpenNIC domain" do
+    expect_errors url, {"opennic" => "http://example.org",
+                        "icann" => "https://example.indy"},
+                  {"opennic" => "Must be under OpenNIC domain",
+                   "icann" => "Must not be under OpenNIC domain"}
+  end
+
+  it "checks for uniqueness of nick and URLs" do
+    overlay = {"nick" => "self", "opennic" => CONFIG.opennic_remote,
+               "icann" => CONFIG.icann_remote}
+    base_errors = Hash(String, String).new "Must be unique"
+    k, keys = overlay.size, overlay.keys
+    until k = 0
+      keys.combinations(k).each do |c|
+        errors = base_errors.select c
+        data = BASE_DATA.merge overlay.select errors.keys
+        expect_errors url, data, errors
+        k -= 1
+      end
+    end
+  end
+
+  it "prevents nick from colliding with HTML headings' id" do
+    msg = "Reserved names: #{HTML_HEADINGS.join ", "}"
+    HTML_HEADINGS.each do |nick|
+      expect_errors url, {"nick" => nick}, {"nick" => msg}
+    end
+  end
+
+  it "inserts to database upon successful application" do
+    overlay = {"nick" => "chad", "opennic" => "http://chad.epic",
+               "icann" => "https://chad.example"}
+    data = BASE_DATA.merge overlay
+    response = HTTP::Client.post url, form: URI::Params.encode data
+    response.status_code.should eq 200
+    response.content_type.should eq "application/xhtml+xml"
+    db = Database.new CONFIG.db
+    db.exec "SELECT opennic, icann, official, left, right
+             FROM member
+             WHERE nick = %Q", overlay["nick"] do |row|
+      row[0].text.should eq overlay["opennic"]
+      row[1].text.should eq overlay["icann"]
+      row[2].int.should eq 0 # applicant
+      row[3].text.should eq "self"
+      row[4].text.should eq "self"
+    end
+  end
+end