Skip to content
48 changes: 27 additions & 21 deletions cmd/main.go
Original file line number Diff line number Diff line change
@@ -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"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[질문]
go/gin에서는 기본적으로 깃허브 레포 통해서 패키지를 관리하나요?! 아니면 따로 설정을 해둔건가요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

server/go.mod

Lines 1 to 4 in 77db124

module github.com/techbloghub/server
go 1.23.4

요기 보면 처음에 프로젝트 초기화할때 패키지 모듈명을 이런식으로 원격저장소 주소로 해서 초기화를 보통 합니당.

공식문서에 적혀있는 내용

In actual development, the module path will typically be the repository location where your source code will be kept. For example, the module path might be github.com/mymodule. If you plan to publish your module for others to use, the module path must be a location from which Go tools can download your module. For more about naming a module with a module path, see Managing dependencies.

링크

요거 물어본거가 맞는거겠죠..?ㅎㅎㅎ

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 맞습니다 ㅎㅎㅎㅎ


"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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리턴 여러개는 더 익숙해저야겠다ㅋㅋㅋㅋㅋㅋㅋ

}

r := gin.Default()
r.GET("/ping", func(context *gin.Context) {
context.String(200, "pong")
})
return r
router.InitRouter(r, client)

return r, client, nil
}
37 changes: 18 additions & 19 deletions cmd/main_test.go
Original file line number Diff line number Diff line change
@@ -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반환 실패")
}
8 changes: 0 additions & 8 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ package config

import (
"fmt"
"log"
"os"

"github.com/joho/godotenv"
)

type Config struct {
Expand Down Expand Up @@ -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"),
Expand Down
42 changes: 42 additions & 0 deletions internal/http/handler/companyhandler.go
Original file line number Diff line number Diff line change
@@ -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,
})
}
}
84 changes: 84 additions & 0 deletions internal/http/handler/companyhandler_test.go
Original file line number Diff line number Diff line change
@@ -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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

testUtils.TransactionalTest 좋은데요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

음 문제될법한 케이스나 좀 더 고민을 해볼 필요는 있을 수 있지만,,, 일단 간단하게 먼저 만들어봤어용

나중에 트랜잭션 관리할때도 withTrnasaction등으로 관리할 수 있는 함수 만들던가 해볼께요.
아마 entgo 공식문서에서도 관련 내용 본것같아서 그 포멧 그대로 가져와볼수도 있고

// 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)
}
}
7 changes: 7 additions & 0 deletions internal/http/handler/pinghandler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package handler

import "github.com/gin-gonic/gin"

func PingPong(context *gin.Context) {
context.String(200, "pong\n")
}
16 changes: 16 additions & 0 deletions internal/http/router/router.go
Original file line number Diff line number Diff line change
@@ -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))
}
19 changes: 19 additions & 0 deletions internal/testutils/config.go
Original file line number Diff line number Diff line change
@@ -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()
}
Comment on lines +9 to +19
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기가 언급해주신 부분이구만요!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넹 일단 테스트용 그냥 하드코딩 하고,, t.Setenv가 테스트 종료후에 원래값으로 바꿔주는것도 있네용

57 changes: 57 additions & 0 deletions internal/testutils/db.go
Original file line number Diff line number Diff line change
@@ -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())
}
Loading