diff --git a/internal/domain/config.go b/internal/domain/config.go index 4672408..d80c371 100644 --- a/internal/domain/config.go +++ b/internal/domain/config.go @@ -4,6 +4,7 @@ import ( "net" "net/netip" "strconv" + "testing" ) type Config struct { @@ -13,6 +14,17 @@ type Config struct { Port uint16 `env:"PORT" envDefault:"3000"` } +func TestConfig(tb testing.TB) *Config { + tb.Helper() + + return &Config{ + ContentDir: "testdata/content/", + Host: "0.0.0.0", + ThemeDir: "testdata/theme/", + Port: 3000, + } +} + func (c Config) AddrPort() netip.AddrPort { return netip.MustParseAddrPort(net.JoinHostPort(c.Host, strconv.FormatUint(uint64(c.Port), 10))) } diff --git a/internal/page/repository/fs/fs_page.go b/internal/page/repository/fs/fs_page.go index d5256a4..10026af 100644 --- a/internal/page/repository/fs/fs_page.go +++ b/internal/page/repository/fs/fs_page.go @@ -39,7 +39,8 @@ func NewFileSystemPageRepository(rootDir fs.FS) page.Repository { func (repo *fileSystemPageRepository) Get(ctx context.Context, lang language.Tag, p string) (*domain.Page, error) { ext := ".md" if lang != language.Und { - ext = "." + lang.String() + ext + base, _ := lang.Base() + ext = "." + base.String() + ext } index := p + ext diff --git a/internal/site/repository/fs/fs_site.go b/internal/site/repository/fs/fs_site.go index 3e44fb7..23fbedf 100644 --- a/internal/site/repository/fs/fs_site.go +++ b/internal/site/repository/fs/fs_site.go @@ -51,7 +51,8 @@ func NewFileSystemSiteRepository(rootDir fs.FS) site.Repository { func (repo *fileSystemSiteRepository) Get(ctx context.Context, lang language.Tag) (*domain.Site, error) { ext := ".md" if lang != language.Und { - ext = "." + lang.String() + ext + base, _ := lang.Base() + ext = "." + base.String() + ext } target := "index" + ext diff --git a/main.go b/main.go index 2506136..62639c9 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,7 @@ import ( "context" "errors" "flag" + "fmt" "io/fs" "log" "net" @@ -39,10 +40,16 @@ import ( themeucase "source.toby3d.me/toby3d/home/internal/theme/usecase" ) -type Context struct { - Site *domain.Site - Page *domain.Page -} +type ( + App struct { + server *http.Server + } + + Context struct { + Site *domain.Site + Page *domain.Page + } +) var ( config = new(domain.Config) @@ -51,43 +58,7 @@ var ( var cpuProfilePath, memProfilePath string -func init() { - flag.StringVar(&cpuProfilePath, "cpuprofile", "", "set path to saving CPU memory profile") - flag.StringVar(&memProfilePath, "memprofile", "", "set path to saving pprof memory profile") - flag.Parse() - - var err error - if err = env.ParseWithOptions(config, env.Options{Prefix: "HOME_"}); err != nil { - logger.Fatalln("cannot unmarshal configuration into domain:", err) - } - - for _, dir := range []*string{ - &config.ContentDir, - &config.ThemeDir, - } { - if *dir, err = filepath.Abs(filepath.Clean(*dir)); err != nil { - logger.Fatalf("cannot format '%s' into absolute path: %s", *dir, err) - } - - if _, err = os.Stat(*dir); err != nil { - if errors.Is(err, os.ErrExist) { - return - } - - if !errors.Is(err, os.ErrNotExist) { - logger.Fatalf("cannot check '%s' path: %v", *dir, err) - } - - if err = os.MkdirAll(*dir, os.ModePerm); err != nil { - logger.Fatalf("cannot create directory on '%s': %v", *dir, err) - } - } - } -} - -func main() { - ctx := context.Background() - +func NewApp(ctx context.Context, config *domain.Config) (*App, error) { themeDir := os.DirFS(config.ThemeDir) contentDir := os.DirFS(config.ContentDir) statics := staticfsrepo.NewFileServerStaticRepository(contentDir) @@ -170,6 +141,72 @@ func main() { WriteTimeout: 500 * time.Millisecond, } + return &App{server: server}, nil +} + +func (a *App) Run(ln net.Listener) error { + var err error + if ln != nil { + err = a.server.Serve(ln) + } else { + err = a.server.ListenAndServe() + } + + if err != nil { + return fmt.Errorf("cannot listen and serve server: %w", err) + } + + return nil +} + +func (a *App) Stop(ctx context.Context) error { + if err := a.server.Shutdown(ctx); err != nil { + return fmt.Errorf("failed to shutdown app: %w", err) + } + + return nil +} + +func main() { + flag.StringVar(&cpuProfilePath, "cpuprofile", "", "set path to saving CPU memory profile") + flag.StringVar(&memProfilePath, "memprofile", "", "set path to saving pprof memory profile") + flag.Parse() + + var err error + if err = env.ParseWithOptions(config, env.Options{Prefix: "HOME_"}); err != nil { + logger.Fatalln("cannot unmarshal configuration into domain:", err) + } + + for _, dir := range []*string{ + &config.ContentDir, + &config.ThemeDir, + } { + if *dir, err = filepath.Abs(filepath.Clean(*dir)); err != nil { + logger.Fatalf("cannot format '%s' into absolute path: %s", *dir, err) + } + + if _, err = os.Stat(*dir); err != nil { + if errors.Is(err, os.ErrExist) { + return + } + + if !errors.Is(err, os.ErrNotExist) { + logger.Fatalf("cannot check '%s' path: %v", *dir, err) + } + + if err = os.MkdirAll(*dir, os.ModePerm); err != nil { + logger.Fatalf("cannot create directory on '%s': %v", *dir, err) + } + } + } + + ctx := context.Background() + + app, err := NewApp(ctx, config) + if err != nil { + logger.Fatalln("cannot setup app:", err) + } + done := make(chan os.Signal, 1) signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) @@ -186,17 +223,17 @@ func main() { defer pprof.StopCPUProfile() } - go func(server *http.Server) { + go func() { logger.Printf("starting server on %d...", config.Port) - if err = server.ListenAndServe(); err != nil { - logger.Fatalln("cannot listen and serve server:", err) + if err = app.Run(nil); err != nil { + logger.Fatalln("cannot run app:", err) } - }(server) + }() <-done - if err = server.Shutdown(ctx); err != nil { + if err = app.Stop(ctx); err != nil { logger.Fatalln("failed shutdown server:", err) } diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..6e1027c --- /dev/null +++ b/main_test.go @@ -0,0 +1,81 @@ +package main_test + +import ( + "context" + "fmt" + "io" + "net" + "net/http" + "strings" + "testing" + "time" + + home "source.toby3d.me/toby3d/home" + "source.toby3d.me/toby3d/home/internal/common" + "source.toby3d.me/toby3d/home/internal/domain" +) + +func TestApp(t *testing.T) { + t.Parallel() + + ln, err := net.Listen("tcp", "0.0.0.0:0") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = ln.Close() }) + + addr, _ := ln.Addr().(*net.TCPAddr) + t.Logf("running testing app on %s port", addr) + + testConfig := domain.TestConfig(t) + testConfig.Host = addr.IP.String() + testConfig.Port = uint16(addr.Port) + + ctx := context.Background() + + app, err := home.NewApp(ctx, testConfig) + if err != nil { + t.Fatal(err) + } + + go app.Run(ln) + t.Cleanup(func() { _ = app.Stop(ctx) }) + + for name, tc := range map[string]struct { + AcceptLanguage string + }{ + "personal": {AcceptLanguage: "ru,en;q=0.7,eo;q=0.3"}, + "mastodon-long": {AcceptLanguage: "en-GB,en-US;q=0.9,en;q=0.8,ru;q=0.7"}, + "mastodon-short": {AcceptLanguage: "en-US,en;q=0.5"}, + } { + name, tc := name, tc + + t.Run(name, func(t *testing.T) { + t.Parallel() + + req, err := http.NewRequest(http.MethodGet, + fmt.Sprintf("http://localhost:%d/", testConfig.Port), nil) + if err != nil { + t.Fatal(err) + } + + req.Header.Set(common.HeaderAcceptLanguage, tc.AcceptLanguage) + + client := http.Client{Timeout: 500 * time.Millisecond} + + resp, err := client.Do(req) + if err != nil { + t.Fatal("cannot make request:", err) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + + if content := string(body); strings.Contains(content, "invalid argument") { + t.Errorf("response body contains error:\n%s", content) + } + }) + } +}