summary refs log tree commit diff homepage
path: root/src
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 /src
parent72e56c93dfda955dd2af05607c7afe2b510fee23 (diff)
downloadhybring-d378edca8215080fb0a86899f6dc52643bdb0852.tar.gz
Add integration tests for HTTP API
Diffstat (limited to 'src')
-rw-r--r--src/cli.cr123
-rw-r--r--src/http.cr18
-rw-r--r--src/sqlite.cr13
-rw-r--r--src/xhtml.cr8
4 files changed, 94 insertions, 68 deletions
diff --git a/src/cli.cr b/src/cli.cr
index 6f5412c..8cc79be 100644
--- a/src/cli.cr
+++ b/src/cli.cr
@@ -40,8 +40,7 @@ struct Configuration
   @icann_remote : String
   getter icann_remote
 
-  def initialize(path)
-    ini = INI.parse File.read path
+  def initialize(ini)
     @db = Path[ini["general"]["db"]]
     @api = ini["general"]["api"]
     @opennic_local = Path[ini["opennic"]["local"]]
@@ -51,68 +50,74 @@ struct Configuration
   end
 end
 
-subcmd = Subcommand::Usage
-usage = ""
-config_path, port = nil, 8910
+def die(error)
+  STDERR << error
+  exit 1
+end
+
+{% unless @top_level.constant("SPEC") %}
+  subcmd = Subcommand::Usage
+  usage = ""
+  config_path, port = nil, 0
 
-parser = OptionParser.new do |parser|
-  banner_prefix = "Usage: #{Path[PROGRAM_NAME].basename}"
-  parser.banner = "#{banner_prefix} [subcommand] [arguments]"
-  parser.on "serve", "start the HTTP server"  do
-    subcmd = Subcommand::Serve
-    parser.banner = "#{banner_prefix} serve --config=PATH [--port=N]"
-    usage = parser.to_s
-    parser.on "-c PATH", "--config=PATH",
-              "path to configuration file (required)" do |path|
-      config_path = path
+  parser = OptionParser.new do |parser|
+    banner_prefix = "Usage: #{Path[PROGRAM_NAME].basename}"
+    parser.banner = "#{banner_prefix} [subcommand] [arguments]"
+    parser.on "serve", "start the HTTP server"  do
+      subcmd = Subcommand::Serve
+      parser.banner = "#{banner_prefix} serve --config=PATH [--port=N]"
+      usage = parser.to_s
+      parser.on "-c PATH", "--config=PATH",
+                "path to configuration file (required)" do |path|
+        config_path = path
+      end
+      parser.on "-p N", "--port=N", "listening port" do |n|
+        port = n.to_i
+      end
     end
-    parser.on "-p N", "--port=N", "listening port (default to 8910)" do |n|
-      port = n.to_i
+    parser.on "-h", "--help", "show this help message and exit"  do
+      puts parser
+      exit
     end
-  end
-  parser.on "-h", "--help", "show this help message and exit"  do
-    puts parser
-    exit
-  end
 
-  parser.invalid_option do |flag|
-    STDERR.puts "Error: Invalid option: #{flag}"
-    subcmd = Subcommand::Usage
-  end
-  parser.missing_option do |flag|
-    STDERR.puts "Error: Missing argument for #{flag}"
-    subcmd = Subcommand::Usage
-  end
-  parser.unknown_args do |before_dash, after_dash|
-    next if before_dash.empty? && after_dash.empty?
-    STDERR << "Error: Unknown arguments:"
-    STDERR << " " << before_dash.join " "  if !before_dash.empty?
-    STDERR << " -- " << after_dash.join " "  if !after_dash.empty?
-    STDERR.puts
-    subcmd = Subcommand::Usage
+    parser.invalid_option do |flag|
+      STDERR.puts "Error: Invalid option: #{flag}"
+      subcmd = Subcommand::Usage
+    end
+    parser.missing_option do |flag|
+      STDERR.puts "Error: Missing argument for #{flag}"
+      subcmd = Subcommand::Usage
+    end
+    parser.unknown_args do |before_dash, after_dash|
+      next if before_dash.empty? && after_dash.empty?
+      STDERR << "Error: Unknown arguments:"
+      STDERR << " " << before_dash.join " "  if !before_dash.empty?
+      STDERR << " -- " << after_dash.join " "  if !after_dash.empty?
+      STDERR.puts
+      subcmd = Subcommand::Usage
+    end
   end
-end
 
-parser.parse
-cfg = case subcmd
-      when .serve?
-        unless config_path
-          STDERR << usage
-          exit 1
+  parser.parse
+  cfg = case subcmd
+        when .serve?
+          unless config_path
+            die usage
+          end
+          begin
+            Configuration.new INI.parse File.read config_path.not_nil!
+          rescue ex
+            die "Error: Failed to read config from #{config_path}: #{ex}\n"
+          end
         end
-        begin
-          Configuration.new config_path.not_nil!
-        rescue ex
-          STDERR.puts "Error: Failed to read config from #{config_path}: #{ex}"
-          exit 1
-        end
-      end
 
-case subcmd
-in .serve?
-  server = Server.new cfg.not_nil!
-  server.listen port
-in .usage?
-  STDERR.puts parser
-  exit 1
-end
+  case subcmd
+  in .serve?
+    server = Server.new cfg.not_nil!
+    server.listen port do |address|
+      puts "Listening on http://#{address}"
+    end
+  in .usage?
+    die parser
+  end
+{% end %}
diff --git a/src/http.cr b/src/http.cr
index 912a46c..248a746 100644
--- a/src/http.cr
+++ b/src/http.cr
@@ -50,6 +50,10 @@ class Server
       content_length = context.request.content_length
       next http_error context, 411 unless content_length
       next http_error context, 413 if content_length > MAX_CONTENT_LENGTH
+      content_type = context.request.headers["Content-Type"]?
+      unless content_type && content_type == "application/x-www-form-urlencoded"
+        next http_error context, 415
+      end
 
       errors = {} of String => String
       params = {} of String => String
@@ -60,7 +64,7 @@ class Server
         case key
         when "nick"
           if value.size > MAX_NICK_LENGTH
-            next errors["nick"] = "Must be within ${MAX_NICK_LENGTH} characters"
+            next errors["nick"] = "Must be within #{MAX_NICK_LENGTH} characters"
           end
           if /^[0-9a-z]+$/ !~ value
             next errors["nick"] = "Must be ASCII lowercase alphanumeric"
@@ -124,7 +128,7 @@ class Server
   getter opennic_page
   getter icann_page
 
-  def listen(port)
+  def listen(port = 0)
     @db.update_hook ->(arg : Void*, action : Database::UpdateAction,
                        db : LibC::Char*, table : LibC::Char*, rowid : Int64) {
       return unless db == "main"
@@ -136,7 +140,15 @@ class Server
       end
     }, self.as Void*
 
-    puts "Listening on http://#{@server.bind_tcp port}"
+    yield @server.bind_tcp port
     @server.listen
   end
+
+  def listening?
+    @server.listening?
+  end
+
+  def close
+    @server.close
+  end
 end
diff --git a/src/sqlite.cr b/src/sqlite.cr
index 9ae1c51..60f2e19 100644
--- a/src/sqlite.cr
+++ b/src/sqlite.cr
@@ -19,6 +19,7 @@
 @[Link("sqlite3")]
 lib SQLite
   OK = 0
+  BUSY = 5
   ROW = 100
   DONE = 101
 
@@ -36,7 +37,7 @@ lib SQLite
   fun free = sqlite3_free(format : Void*, ...)
 
   fun open = sqlite3_open(filename : LibC::Char*, db : Database*) : LibC::Int
-  fun close = sqlite3_close(db : Database) : LibC::Int
+  fun close = sqlite3_close_v2(db : Database) : LibC::Int
   fun update_hook = sqlite3_update_hook(db : Database,
                                         f : (Void*, UpdateAction, LibC::Char*,
                                              LibC::Char*, Int64 ->),
@@ -119,6 +120,12 @@ class Database
     end
   end
 
+  def initialize(path)
+    Dir.mkdir_p path.parent
+    Database.check SQLite.open path.to_s, out @ref
+    self.exec "PRAGMA foreign_keys = ON" do end
+  end
+
   def initialize(path, opennic, icann)
     Dir.mkdir_p path.parent
     Database.check SQLite.open path.to_s, out @ref
@@ -162,6 +169,8 @@ class Database
         yield stmt.row
       when SQLite::DONE
         break
+      when SQLite::BUSY
+        next # FIXME: rollback transaction
       else
         Database.check rc
       end
@@ -169,7 +178,7 @@ class Database
   end
 
   def transact
-    self.exec "BEGIN TRANSACTION" do end
+    self.exec "BEGIN" do end
     yield
     self.exec "COMMIT" do end
   end
diff --git a/src/xhtml.cr b/src/xhtml.cr
index d53eb04..8db5995 100644
--- a/src/xhtml.cr
+++ b/src/xhtml.cr
@@ -32,9 +32,9 @@ CSS = "
           display: grid;
           grid-template-columns: max-content 1fr 0;
       }
-      form label { margin-right: 1ch }
       form input { margin-bottom: 1ex }
-      .error {
+      form label { margin-right: 1ch }
+      form label.error {
         color: red;
         margin-top: -1ex;
         margin-bottom: 1ex;
@@ -90,8 +90,8 @@ class Page
                 required: "required"
     xml.element "br"
     if error
-      xml.element "label", class: "error" do xml.text "Error:" end
-      xml.element "label", class: "error" do xml.text error end
+      xml.element "label", for: name, class: "error" do xml.text "Error:" end
+      xml.element "label", for: name, class: "error" do xml.text error end
       xml.element "br"
     end
   end