# 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 . require "file_utils" require "http/client" require "socket" 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(client, overlay, expected) data = BASE_DATA.merge overlay response = client.post "/", 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 client = uninitialized HTTP::Client Spec.before_suite do server = Server.new CONFIG spawn do server.listen do end end until server.listening? sleep 1.milliseconds end client = HTTP::Client.new UNIXSocket.new CONFIG.sock Spec.after_suite do server.close FileUtils.rm_r [CONFIG.db, CONFIG.opennic_local, CONFIG.icann_local] end end describe Server do pending "only accepts POST requests" do %w(GET PUT HEAD DELETE PATCH OPTIONS).each do |method| # FIXME: Unsupported HTTP version: 405 (Exception)??? response = client.exec method, "/" 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 pending "rejects too long content" do # FIXME: Race condition in HTTP::Client response = client.post "/", 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 = client.post "/" 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 = client.post "/", 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 = client.post "/", 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 = client.post "/", 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 client, {"nick" => "x" * (MAX_NICK_LENGTH + 1)}, {"nick" => "Must match #{NICK_PATTERN}"} end it "rejects nicks that are not lowercase alphanumeric" do %w(camelCase pun/tu?a#tion).each do |nick| expect_errors client, {"nick" => nick}, {"nick" => "Must match #{NICK_PATTERN}"} end end it "rejects relative URLs" do %w(foo /bar ex.am/ple).each do |uri| expect_errors client, {"opennic" => uri, "icann" => uri}, {"opennic" => "Must be absolute URL", "icann" => "Must be absolute URL"} end end it "only accepts HTTP/S" do expect_errors client, {"opennic" => "gopher://example.indy"}, {"opennic" => "Must be HTTP/S"} expect_errors client, {"opennic" => "https://example.indy", "icann" => "http://example.org"}, {"icann" => "Must be HTTPS"} end it "checks for OpenNIC domain" do expect_errors client, {"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 client, 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 client, {"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 = client.post "/", 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