diff --git a/cmd/main.go b/cmd/main.go index 98ce422..263a1ff 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,43 +1,49 @@ package main import ( - "fmt" "log" + "github.com/joho/godotenv" "github.com/techbloghub/server/config" + "github.com/techbloghub/server/ent" _ "github.com/techbloghub/server/ent/runtime" "github.com/techbloghub/server/internal/database" + "github.com/techbloghub/server/internal/http/router" "github.com/gin-gonic/gin" _ "github.com/lib/pq" ) func main() { - cfg, cfgErr := config.NewConfig() - if cfgErr != nil { - log.Fatalf("failed to load config: %v", cfgErr) + errEnv := godotenv.Load(".env") + if errEnv != nil { + log.Print("failed to reading .env", errEnv) } - // DB 연결 - client, errPg := database.ConnectDatabase(cfg) - if errPg != nil { - log.Fatalf("failed to connect database: %v", errPg) + cfg, err := config.NewConfig() + if err != nil { + log.Fatalf("failed to load config: %v", err) } - defer client.Close() - - // 서버 실행 - r := setRouter() - routerErr := r.Run(":" + cfg.ServerConfig.Port) - if routerErr != nil { - fmt.Println("Error while running server: ", cfgErr) - return + + r, dbClient, err := createServer(cfg) + if err != nil { + log.Fatalf("failed to create server: %v", err) + } + defer dbClient.Close() + + if err := r.Run(":" + cfg.ServerConfig.Port); err != nil { + log.Fatalf("Error while running server: %v", err) } } -func setRouter() *gin.Engine { +func createServer(cfg *config.Config) (*gin.Engine, *ent.Client, error) { + client, err := database.ConnectDatabase(cfg) + if err != nil { + return nil, nil, err + } + r := gin.Default() - r.GET("/ping", func(context *gin.Context) { - context.String(200, "pong") - }) - return r + router.InitRouter(r, client) + + return r, client, nil } diff --git a/cmd/main_test.go b/cmd/main_test.go index 39b29ce..3badc44 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -1,34 +1,33 @@ package main import ( + "fmt" "net/http" "net/http/httptest" "testing" - "github.com/gin-gonic/gin" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/techbloghub/server/internal/testutils" ) -func TestPingEndpoint(t *testing.T) { - // Set Gin to Test Mode - gin.SetMode(gin.TestMode) +func TestMainIntegration(t *testing.T) { + cfg, err := testutils.NewTestConfig(t) - // Create a new router instance using the private setRouter function - router := setRouter() + require.NoError(t, err, "config 로드 실패") - // Create a test HTTP recorder - w := httptest.NewRecorder() + r, client, err := createServer(cfg) + require.NoError(t, err, "서버 생성중 에러 발생") + require.NotNil(t, r, "gin server생성 실패") + require.NotNil(t, client, "db client 생성 실패") - // Create a test request to the /ping endpoint - req, err := http.NewRequest("GET", "/ping", nil) - assert.NoError(t, err) + defer client.Close() - // Serve the request using the router - router.ServeHTTP(w, req) + ts := httptest.NewServer(r) + defer ts.Close() - // Assert the response code is 200 - assert.Equal(t, http.StatusOK, w.Code) - - // Assert the response body is "pong" - assert.Equal(t, "pong", w.Body.String()) + // Make a request to /ping to ensure everything is wired up + url := fmt.Sprintf("%s/ping", ts.URL) + resp, err := http.Get(url) + require.NoError(t, err, "/ping 호출중 에러 발생") + require.Equal(t, http.StatusOK, resp.StatusCode, "200 OK반환 실패") } diff --git a/config/config.go b/config/config.go index be2095c..fae477e 100644 --- a/config/config.go +++ b/config/config.go @@ -2,10 +2,7 @@ package config import ( "fmt" - "log" "os" - - "github.com/joho/godotenv" ) type Config struct { @@ -44,11 +41,6 @@ func (cfg *ServerConfig) ToMap() map[string]string { } func NewConfig() (*Config, error) { - errEnv := godotenv.Load(".env") - if errEnv != nil { - log.Print("failed to reading .env", errEnv) - } - postgresConf := PostgresConfig{ Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), diff --git a/internal/http/handler/companyhandler.go b/internal/http/handler/companyhandler.go new file mode 100644 index 0000000..fbde4db --- /dev/null +++ b/internal/http/handler/companyhandler.go @@ -0,0 +1,42 @@ +package handler + +import ( + "github.com/gin-gonic/gin" + "github.com/techbloghub/server/ent" +) + +// Company 정보 Response 구조체 +type CompanyResponse struct { + ID int `json:"id"` + Name string `json:"name"` + LogoURL string `json:"logo_url"` + BlogURL string `json:"blog_url"` +} + +type CompanyListResponse struct { + Companies []CompanyResponse `json:"companies"` +} + +func ListCompanies(client *ent.Client) gin.HandlerFunc { + return func(c *gin.Context) { + entCompanies, err := client.Company.Query().All(c) + if err != nil { + c.JSON(500, gin.H{"error": err}) + return + } + + companies := make([]CompanyResponse, len(entCompanies)) + for i, company := range entCompanies { + companies[i] = CompanyResponse{ + ID: company.ID, + Name: company.Name, + LogoURL: company.LogoURL.String(), + BlogURL: company.BlogURL.String(), + } + } + + c.JSON(200, CompanyListResponse{ + Companies: companies, + }) + } +} diff --git a/internal/http/handler/companyhandler_test.go b/internal/http/handler/companyhandler_test.go new file mode 100644 index 0000000..02cb609 --- /dev/null +++ b/internal/http/handler/companyhandler_test.go @@ -0,0 +1,84 @@ +package handler_test + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "github.com/techbloghub/server/ent" + "github.com/techbloghub/server/internal/http/handler" + "github.com/techbloghub/server/internal/testutils" +) + +func TestListCompanies(t *testing.T) { + testutils.TransactionalTest(t, func(t *testing.T, client *ent.Client) { + // seedn data 추가 + seedCompanies(t, client) + + // Create a new Gin router for test environment + gin.SetMode(gin.TestMode) + r := gin.New() + r.GET("/companies", handler.ListCompanies(client)) + + // Create a new HTTP request + req, _ := http.NewRequest("GET", "/companies", nil) + + // Create a response recorder + w := httptest.NewRecorder() + + // Perform the request + r.ServeHTTP(w, req) + + // Check the status code + assert.Equal(t, http.StatusOK, w.Code) + + // Check the response body + var actualResponse handler.CompanyListResponse + err := json.Unmarshal(w.Body.Bytes(), &actualResponse) + if err != nil { + t.Fatalf("failed to unmarshal response: %v", err) + } + + expectedResponse := []map[string]interface{}{ + { + "name": "Company A", + "logo_url": "http://example.com/logoA.png", + "blog_url": "http://example.com/blogA", + }, + { + "name": "Company B", + "logo_url": "http://example.com/logoB.png", + "blog_url": "http://example.com/blogB", + }, + } + + for i, company := range actualResponse.Companies { + assert.Equal(t, expectedResponse[i]["name"], company.Name) + assert.Equal(t, expectedResponse[i]["logo_url"], company.LogoURL) + assert.Equal(t, expectedResponse[i]["blog_url"], company.BlogURL) + } + }) +} + +func seedCompanies(t *testing.T, client *ent.Client) { + _, err := client.Company.CreateBulk( + client.Company.Create(). + SetName("Company A"). + SetLogoURL(&url.URL{Scheme: "http", Host: "example.com", Path: "/logoA.png"}). + SetBlogURL(&url.URL{Scheme: "http", Host: "example.com", Path: "/blogA"}). + SetRssURL(&url.URL{Scheme: "http", Host: "example.com", Path: "/rssA"}), + client.Company.Create(). + SetName("Company B"). + SetLogoURL(&url.URL{Scheme: "http", Host: "example.com", Path: "/logoB.png"}). + SetBlogURL(&url.URL{Scheme: "http", Host: "example.com", Path: "/blogB"}). + SetRssURL(&url.URL{Scheme: "http", Host: "example.com", Path: "/rssB"}), + ).Save(context.Background()) + if err != nil { + t.Fatalf("failed to seed companies: %v", err) + } +} diff --git a/internal/http/handler/pinghandler.go b/internal/http/handler/pinghandler.go new file mode 100644 index 0000000..fa413fe --- /dev/null +++ b/internal/http/handler/pinghandler.go @@ -0,0 +1,7 @@ +package handler + +import "github.com/gin-gonic/gin" + +func PingPong(context *gin.Context) { + context.String(200, "pong\n") +} diff --git a/internal/http/router/router.go b/internal/http/router/router.go new file mode 100644 index 0000000..0e009e8 --- /dev/null +++ b/internal/http/router/router.go @@ -0,0 +1,16 @@ +package router + +import ( + "github.com/gin-gonic/gin" + "github.com/techbloghub/server/ent" + "github.com/techbloghub/server/internal/http/handler" +) + +func InitRouter(r *gin.Engine, client *ent.Client) { + // PingPong 테스트 + r.GET("/ping", handler.PingPong) + + // 회사 리스트 조회 + // curl -X GET http://localhost:8080/companies + r.GET("/companies", handler.ListCompanies(client)) +} diff --git a/internal/testutils/config.go b/internal/testutils/config.go new file mode 100644 index 0000000..5ef2d73 --- /dev/null +++ b/internal/testutils/config.go @@ -0,0 +1,19 @@ +package testutils + +import ( + "testing" + + "github.com/techbloghub/server/config" +) + +func NewTestConfig(t *testing.T) (*config.Config, error) { + t.Setenv("ENV", "test") + t.Setenv("PORT", "8081") + t.Setenv("POSTGRES_HOST", "localhost") + t.Setenv("POSTGRES_USER", "example-user") + t.Setenv("POSTGRES_PASSWORD", "password") + t.Setenv("POSTGRES_DB", "tbh-db") + t.Setenv("POSTGRES_PORT", "5433") + + return config.NewConfig() +} diff --git a/internal/testutils/db.go b/internal/testutils/db.go new file mode 100644 index 0000000..0fd1539 --- /dev/null +++ b/internal/testutils/db.go @@ -0,0 +1,57 @@ +package testutils + +import ( + "context" + "fmt" + "testing" + + _ "github.com/lib/pq" + "github.com/techbloghub/server/ent" + "github.com/techbloghub/server/ent/enttest" +) + +func SetupDB(t *testing.T) (*ent.Client, *ent.Tx) { + // test env 설정 + + cfg, err := NewTestConfig(t) + if err != nil { + t.Fatalf("config 로딩 실패: %v", err) + } + pgCfg := cfg.PostgresConfig + + // enttest: client 생성 & migration 실행 + client := enttest.Open(t, "postgres", fmt.Sprintf("host=%s port=%s user=%s dbname=%s password=%s sslmode=disable", + pgCfg.Host, pgCfg.Port, pgCfg.User, pgCfg.Db, pgCfg.Password)) + + // transaction 시작 + tx, err := client.Tx(context.Background()) + if err != nil { + t.Fatalf("트랜잭션 시작 실패: %v", err) + } + + return client, tx +} + +func TearDown(client *ent.Client, tx *ent.Tx) { + tx.Rollback() + client.Close() +} + +/** +* TransactionalTest는 트랜잭션을 사용하여 테스트를 실행하는 함수입니다. +* 테스트가 종료된 후 트랜잭션을 롤백하여 데이터베이스 상태를 원래대로 복원합니다. +* +* @param t *testing.T - 테스트 핸들러 +* @param fn func(t *testing.T, client *ent.Client) - 테스트 함수, ent.Client를 인자로 받습니다. + */ +func TransactionalTest(t *testing.T, fn func(t *testing.T, client *ent.Client)) { + client, tx := SetupDB(t) + defer func() { + if err := tx.Rollback(); err != nil { + t.Fatalf("트랜잭션 롤백 실패: %v", err) + } + client.Close() + }() + + fn(t, tx.Client()) +}