feat(api): wire server configuration and dependencies

This commit is contained in:
2026-09-21 14:15:30 +03:30
parent 5dbfe0a12f
commit 3f29a5ec5f
6 changed files with 225 additions and 7 deletions

68
backend/cmd/api/config.go Normal file
View File

@@ -0,0 +1,68 @@
package main
import (
"fmt"
"os"
"strings"
"time"
)
type config struct {
ServerAddr string
FrontendOrigin string
ZarinHubEndpoint string
ZarinHubToken string
ZarinHubTimeout time.Duration
DatabasePath string
}
func loadConfig() (config, error) {
return readConfig(os.Getenv)
}
func readConfig(getenv func(string) string) (config, error) {
token := strings.TrimSpace(getenv("ZARINHUB_ACCESS_TOKEN"))
if token == "" {
return config{}, fmt.Errorf("ZARINHUB_ACCESS_TOKEN is required")
}
timeoutText := valueOrDefault(
getenv("ZARINHUB_TIMEOUT"),
"5s",
)
timeout, err := time.ParseDuration(timeoutText)
if err != nil || timeout <= 0 {
return config{}, fmt.Errorf(
"ZARINHUB_TIMEOUT must be a positive duration",
)
}
return config{
ServerAddr: valueOrDefault(
getenv("SERVER_ADDR"),
"127.0.0.1:8080",
),
FrontendOrigin: valueOrDefault(
getenv("FRONTEND_ORIGIN"),
"http://127.0.0.1:3000",
),
ZarinHubEndpoint: valueOrDefault(
getenv("ZARINHUB_ENDPOINT"),
"https://zarin-hub.com/api/v5/KYC/CardToIban",
),
ZarinHubToken: token,
ZarinHubTimeout: timeout,
DatabasePath: valueOrDefault(
getenv("DATABASE_PATH"),
"./card-to-iban.db",
),
}, nil
}
func valueOrDefault(value string, fallback string) string {
if strings.TrimSpace(value) == "" {
return fallback
}
return strings.TrimSpace(value)
}

View File

@@ -0,0 +1,54 @@
package main
import (
"strings"
"testing"
"time"
)
func TestReadConfigUsesDefaults(t *testing.T) {
values := map[string]string{
"ZARINHUB_ACCESS_TOKEN": "test-token",
}
config, err := readConfig(func(key string) string {
return values[key]
})
if err != nil {
t.Fatalf("readConfig() error = %v", err)
}
if config.ServerAddr != "127.0.0.1:8080" {
t.Errorf("ServerAddr = %q", config.ServerAddr)
}
if config.FrontendOrigin != "http://127.0.0.1:3000" {
t.Errorf("FrontendOrigin = %q", config.FrontendOrigin)
}
if config.ZarinHubTimeout != 5*time.Second {
t.Errorf("ZarinHubTimeout = %v", config.ZarinHubTimeout)
}
if config.DatabasePath != "./card-to-iban.db" {
t.Errorf("DatabasePath = %q", config.DatabasePath)
}
}
func TestReadConfigRejectsMissingAccessToken(t *testing.T) {
_, err := readConfig(func(key string) string { return "" })
if err == nil || !strings.Contains(err.Error(), "ZARINHUB_ACCESS_TOKEN") {
t.Fatalf("readConfig() error = %v, want missing token error", err)
}
}
func TestReadConfigRejectsInvalidTimeout(t *testing.T) {
values := map[string]string{
"ZARINHUB_ACCESS_TOKEN": "test-token",
"ZARINHUB_TIMEOUT": "later",
}
_, err := readConfig(func(key string) string {
return values[key]
})
if err == nil || !strings.Contains(err.Error(), "ZARINHUB_TIMEOUT") {
t.Fatalf("readConfig() error = %v, want timeout error", err)
}
}

View File

@@ -1,18 +1,103 @@
package main
import (
"context"
"encoding/json"
"log"
"errors"
"fmt"
"log/slog"
"net/http"
"os"
"time"
"card-to-iban/internal/httpapi"
"card-to-iban/internal/repository"
"card-to-iban/internal/service"
"card-to-iban/internal/storage"
"card-to-iban/internal/zarinhub"
"github.com/joho/godotenv"
)
func main() {
http.HandleFunc("/health", healthHandler)
logger := slog.New(
slog.NewJSONHandler(os.Stdout, nil),
)
log.Println("server listening on http://localhost:8080")
if err := run(logger); err != nil {
logger.Error("server stopped", "error", err)
os.Exit(1)
}
}
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
func run(logger *slog.Logger) error {
loadLocalEnvironment()
config, err := loadConfig()
if err != nil {
return fmt.Errorf("load configuration: %w", err)
}
db, err := storage.OpenSQLite(config.DatabasePath)
if err != nil {
return err
}
migrationContext, cancelMigration := context.WithTimeout(
context.Background(),
5*time.Second,
)
defer cancelMigration()
if err := storage.Migrate(migrationContext, db); err != nil {
if closeErr := db.Close(); closeErr != nil {
return errors.Join(err, closeErr)
}
return err
}
zarinHubClient := zarinhub.NewClient(
&http.Client{Timeout: config.ZarinHubTimeout},
config.ZarinHubEndpoint,
config.ZarinHubToken,
)
cardService := service.NewCardService(zarinHubClient)
auditRepository := repository.NewAuditRepository(db)
handler := httpapi.NewHandler(
cardService,
auditRepository,
logger,
)
mux := http.NewServeMux()
mux.HandleFunc("/health", healthHandler)
mux.HandleFunc("/api/v1/card-to-iban", handler.Convert)
server := &http.Server{
Addr: config.ServerAddr,
Handler: httpapi.AllowOrigin(
config.FrontendOrigin,
mux,
),
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
logger.Info("server listening", "address", config.ServerAddr)
serverErr := server.ListenAndServe()
closeErr := db.Close()
return errors.Join(serverErr, closeErr)
}
func loadLocalEnvironment() {
for _, path := range []string{".env", "../.env"} {
if err := godotenv.Load(path); err == nil {
return
}
}
}
@@ -29,6 +114,10 @@ func healthHandler(w http.ResponseWriter, r *http.Request) {
}
if err := json.NewEncoder(w).Encode(response); err != nil {
log.Println("failed to encode health response:", err)
slog.ErrorContext(
r.Context(),
"health response encoding failed",
"error", err,
)
}
}

View File

@@ -2,7 +2,10 @@ module card-to-iban
go 1.27.1
require modernc.org/sqlite v1.59.0
require (
github.com/joho/godotenv v1.5.1
modernc.org/sqlite v1.59.0
)
require (
github.com/dustin/go-humanize v1.0.1 // indirect

View File

@@ -6,6 +6,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=