diff --git a/app/app.go b/app/app.go deleted file mode 100644 index 5260e708..00000000 --- a/app/app.go +++ /dev/null @@ -1,62 +0,0 @@ -// Package app is the entry point of the application. -package app - -import ( - "fmt" - "net/http" - - "github.com/gulien/gotenberg/app/config" - "github.com/gulien/gotenberg/app/handlers" - "github.com/gulien/gotenberg/app/handlers/converter/process" - "github.com/gulien/gotenberg/app/logger" - - "github.com/gorilla/mux" -) - -// App gathers all data required for the application. -type App struct { - // version is the application version as defined in the main package. - version string - // config is the application configuration. - // It's populated thanks to the gotenberg.yml file. - config *config.AppConfig - // Server is the instance of http.Server used by the application. - Server *http.Server -} - -// NewApp instantiates the application by loading the configuration from the -// gotenberg.yml file. -func NewApp(version string, configurationFilePath string) (*App, error) { - c, err := config.NewAppConfig(configurationFilePath) - if err != nil { - return nil, err - } - - a := &App{} - a.version = version - a.config = c - - // defines our application logging. - logger.SetLevel(a.config.Logs.Level) - logger.SetFormatter(a.config.Logs.Formatter) - - // defines our application router. - r := mux.NewRouter() - r.Handle("/", handlers.GetHandlersChain()) - - a.Server = &http.Server{ - Addr: fmt.Sprintf(":%s", a.config.Port), - Handler: r, - } - - return a, nil -} - -// Run starts the server. -func (a *App) Run() error { - process.Load(a.config.CommandsConfig) - logger.Infof("Starting Gotenberg version %s", a.version) - logger.Infof("Listening on port %s", a.config.Port) - - return a.Server.ListenAndServe() -} diff --git a/app/app_test.go b/app/app_test.go deleted file mode 100644 index 19fbdb18..00000000 --- a/app/app_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package app - -import ( - "path/filepath" - "testing" -) - -func TestNewApp(t *testing.T) { - // case 1: uses an empty configuration file path. - if _, err := NewApp("tests", ""); err == nil { - t.Error("App should not have been instantiated!") - } - - // case 2: uses an correct configuration file. - path, _ := filepath.Abs("../_tests/configurations/gotenberg.yml") - if _, err := NewApp("tests", path); err != nil { - t.Error("App should have been instantiated!") - } -} - -func TestRun(t *testing.T) { - path, _ := filepath.Abs("../_tests/configurations/gotenberg.yml") - a, _ := NewApp("tests", path) - - quit := make(chan bool, 1) - go func() { - a.Run() - }() - quit <- true -} diff --git a/app/handlers/context/context.go b/app/context/context.go similarity index 95% rename from app/handlers/context/context.go rename to app/context/context.go index f9036b73..ff34bc33 100644 --- a/app/handlers/context/context.go +++ b/app/context/context.go @@ -6,9 +6,9 @@ import ( "context" "net/http" - "github.com/gulien/gotenberg/app/handlers/converter" + "github.com/gulien/gotenberg/app/converter" - ghttp "github.com/gulien/gotenberg/app/handlers/http" + ghttp "github.com/gulien/gotenberg/app/http" ) type key uint32 diff --git a/app/handlers/converter/converter.go b/app/converter/converter.go similarity index 93% rename from app/handlers/converter/converter.go rename to app/converter/converter.go index b826672a..0e1b3e6b 100644 --- a/app/handlers/converter/converter.go +++ b/app/converter/converter.go @@ -6,9 +6,9 @@ import ( "net/http" "os" - gfile "github.com/gulien/gotenberg/app/handlers/converter/file" - "github.com/gulien/gotenberg/app/handlers/converter/process" - ghttp "github.com/gulien/gotenberg/app/handlers/http" + gfile "github.com/gulien/gotenberg/app/converter/file" + "github.com/gulien/gotenberg/app/converter/process" + ghttp "github.com/gulien/gotenberg/app/http" "github.com/satori/go.uuid" ) diff --git a/app/handlers/converter/file/file.go b/app/converter/file/file.go similarity index 98% rename from app/handlers/converter/file/file.go rename to app/converter/file/file.go index c7c12156..b1d5b7ca 100644 --- a/app/handlers/converter/file/file.go +++ b/app/converter/file/file.go @@ -6,7 +6,7 @@ import ( "io" "os" - ghttp "github.com/gulien/gotenberg/app/handlers/http" + ghttp "github.com/gulien/gotenberg/app/http" "github.com/satori/go.uuid" ) diff --git a/app/converter/file/file_test.go b/app/converter/file/file_test.go new file mode 100644 index 00000000..4f99f76a --- /dev/null +++ b/app/converter/file/file_test.go @@ -0,0 +1,52 @@ +package file + +import ( + "bytes" + "os" + "path/filepath" + "testing" +) + +func TestNewFile(t *testing.T) { + workingDir := "test" + os.Mkdir(workingDir, 0666) + + // case 1: uses an empty reader. + if _, err := NewFile(workingDir, new(bytes.Buffer)); err == nil { + t.Error("File should not have been instantiated!") + } + + // case 2: uses a reader from a wrong file type. + path, _ := filepath.Abs("../../../_tests/configurations/gotenberg.yml") + r, _ := os.Open(path) + defer r.Close() + if _, err := NewFile(workingDir, r); err == nil { + t.Error("File should not have been instantiated!") + } + + // case 3: uses a reader from a correct file type. + path, _ = filepath.Abs("../../../_tests/file.pdf") + r, _ = os.Open(path) + defer r.Close() + if _, err := NewFile(workingDir, r); err != nil { + t.Error("File should have been instantiated!") + } + + os.RemoveAll(workingDir) +} + +func TestReworkFilePath(t *testing.T) { + workingDir := "test" + os.Mkdir(workingDir, 0666) + + f := &File{ + Path: MakeFilePath(workingDir), + Type: 999, + } + + if _, err := reworkFilePath(workingDir, f); err == nil { + t.Error("It should not have been able to found the file extension!") + } + + os.RemoveAll(workingDir) +} diff --git a/app/handlers/converter/process/process.go b/app/converter/process/process.go similarity index 97% rename from app/handlers/converter/process/process.go rename to app/converter/process/process.go index e019f222..d2842cc4 100644 --- a/app/handlers/converter/process/process.go +++ b/app/converter/process/process.go @@ -9,7 +9,7 @@ import ( "time" "github.com/gulien/gotenberg/app/config" - gfile "github.com/gulien/gotenberg/app/handlers/converter/file" + gfile "github.com/gulien/gotenberg/app/converter/file" ) var commandsConfig *config.CommandsConfig diff --git a/app/handlers/handlers.go b/app/handlers.go similarity index 93% rename from app/handlers/handlers.go rename to app/handlers.go index c5b8a7ce..17d56eae 100644 --- a/app/handlers/handlers.go +++ b/app/handlers.go @@ -1,5 +1,5 @@ -// Package handlers implements all functions on which a request will pass through. -package handlers +// Package app implements all functions on which a request will pass through. +package app import ( "fmt" @@ -7,9 +7,9 @@ import ( "net/http" "os" - "github.com/gulien/gotenberg/app/handlers/context" - "github.com/gulien/gotenberg/app/handlers/converter" - ghttp "github.com/gulien/gotenberg/app/handlers/http" + "github.com/gulien/gotenberg/app/context" + "github.com/gulien/gotenberg/app/converter" + ghttp "github.com/gulien/gotenberg/app/http" "github.com/gulien/gotenberg/app/logger" "github.com/justinas/alice" diff --git a/app/handlers_test.go b/app/handlers_test.go new file mode 100644 index 00000000..8c937302 --- /dev/null +++ b/app/handlers_test.go @@ -0,0 +1,108 @@ +package app + +import ( + "io" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/gulien/gotenberg/app/context" + ghttp "github.com/gulien/gotenberg/app/http" + + "github.com/justinas/alice" +) + +func fakeSuccessHandler(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) +} + +func makeBody(filesPaths ...string) io.Reader { + if len(filesPaths) == 1 { + file, _ := os.Open(filesPaths[0]) + defer file.Close() + return file + } + + // TODO + return nil +} + +func TestEnforceContentLengthHandler(t *testing.T) { + h := alice.New(enforceContentLengthHandler).ThenFunc(fakeSuccessHandler) + + // case 1: sends an empty request. + req := httptest.NewRequest(http.MethodPost, "/", nil) + rr := httptest.NewRecorder() + h.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusBadRequest { + t.Errorf("Handler returned wrong status code: got %v want %v", status, http.StatusBadRequest) + } + + // case 2: sends a body. + path, _ := filepath.Abs("../../_tests/file.docx") + req = httptest.NewRequest(http.MethodPost, "/", makeBody(path)) + rr = httptest.NewRecorder() + h.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusOK { + t.Errorf("Handler returned wrong status code: got %v want %v", status, http.StatusOK) + } +} + +func TestEnforceContentTypeHandler(t *testing.T) { + h := alice.New(enforceContentTypeHandler).ThenFunc(fakeSuccessHandler) + + // case 1: sends a wrong content type. + req := httptest.NewRequest(http.MethodPost, "/", nil) + req.Header.Set("Content-Type", string(ghttp.PDFContentType)) + rr := httptest.NewRecorder() + h.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusUnsupportedMediaType { + t.Errorf("Handler returned wrong status code: got %v want %v", status, http.StatusUnsupportedMediaType) + } + + // case 2: sends a good content type. + req = httptest.NewRequest(http.MethodPost, "/", nil) + req.Header.Set("Content-Type", string(ghttp.OctetStreamContentType)) + rr = httptest.NewRecorder() + h.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusOK { + t.Errorf("Handler returned wrong status code: got %v want %v", status, http.StatusOK) + } +} + +func TestConvertHandler(t *testing.T) { + h := alice.New(convertHandler).ThenFunc(fakeSuccessHandler) + + // case 1: sends a request without a content type in its context. + req := httptest.NewRequest(http.MethodPost, "/", nil) + rr := httptest.NewRecorder() + h.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusInternalServerError { + t.Errorf("Handler returned wrong status code: got %v want %v", status, http.StatusInternalServerError) + } + + // case 2: sends a request as without body. + req = context.WithContentType(httptest.NewRequest(http.MethodPost, "/", nil), ghttp.OctetStreamContentType) + rr = httptest.NewRecorder() + h.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusInternalServerError { + t.Errorf("Handler returned wrong status code: got %v want %v", status, http.StatusInternalServerError) + } + + // case 3: sends a request as "multipart/form-data" without "files" key. + /*req = context.WithContentType(httptest.NewRequest(http.MethodPost, "/", nil), ghttp.MultipartFormDataContentType) + rr = httptest.NewRecorder() + h.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusBadRequest { + t.Errorf("Handler returned wrong status code: got %v want %v", status, http.StatusBadRequest) + }*/ +} diff --git a/app/handlers/http/http.go b/app/http/http.go similarity index 100% rename from app/handlers/http/http.go rename to app/http/http.go diff --git a/main.go b/main.go index cf0fe35c..d17f03c9 100644 --- a/main.go +++ b/main.go @@ -9,13 +9,18 @@ package main import ( "context" + "fmt" + "net/http" "os" "os/signal" "time" "github.com/gulien/gotenberg/app" + "github.com/gulien/gotenberg/app/config" + "github.com/gulien/gotenberg/app/converter/process" "github.com/gulien/gotenberg/app/logger" + "github.com/gorilla/mux" "github.com/sirupsen/logrus" ) @@ -28,29 +33,47 @@ const defaultConfigurationFilePath = "gotenberg.yml" // main initializes the application, starts it, and handles // graceful shutdown. func main() { - a, err := app.NewApp(version, defaultConfigurationFilePath) + c, err := config.NewAppConfig(defaultConfigurationFilePath) if err != nil { logger.SetLevel(logrus.InfoLevel) logger.Fatal(err) os.Exit(1) } + // defines our application logging. + logger.SetLevel(c.Logs.Level) + logger.SetFormatter(c.Logs.Formatter) + + // defines our application router. + r := mux.NewRouter() + r.Handle("/", app.GetHandlersChain()).Methods(http.MethodPost) + + // defines our server. + s := &http.Server{ + Addr: fmt.Sprintf(":%s", c.Port), + Handler: r, + } + + process.Load(c.CommandsConfig) + logger.Infof("Starting Gotenberg version %s", version) + logger.Infof("Listening on port %s", c.Port) + // runs our server in a goroutine so that it doesn't block. go func() { - if err = a.Run(); err != nil { + if err = s.ListenAndServe(); err != nil { logger.SetLevel(logrus.InfoLevel) logger.Panic(err) os.Exit(1) } }() - c := make(chan os.Signal, 1) + quit := make(chan os.Signal, 1) // we'll accept graceful shutdowns when quit via SIGINT (Ctrl+C) // SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught. - signal.Notify(c, os.Interrupt) + signal.Notify(quit, os.Interrupt) // blocks until we receive our signal. - <-c + <-quit // creates a deadline to wait for. var wait time.Duration @@ -59,7 +82,7 @@ func main() { // doesn't block if no connections, but will otherwise wait // until the timeout deadline. - a.Server.Shutdown(ctx) + s.Shutdown(ctx) logger.SetLevel(logrus.InfoLevel) logger.Info("Bye!")