From 5760f1c5f4b8c9df013730f742fcff7b3da9fed3 Mon Sep 17 00:00:00 2001 From: Hikaru Saito Date: Fri, 6 Feb 2026 17:13:08 +0900 Subject: [PATCH 01/18] =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 3 +++ 1 file changed, 3 insertions(+) diff --git a/go.mod b/go.mod index cb09af0..2189653 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/joho/godotenv v1.5.1 github.com/oapi-codegen/gin-middleware v1.0.2 github.com/oapi-codegen/runtime v1.1.2 + github.com/stretchr/testify v1.11.1 google.golang.org/api v0.231.0 ) @@ -34,6 +35,7 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect @@ -72,6 +74,7 @@ require ( github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/quic-go v0.55.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect From e121872b77c9ac5ba3851c12f50add7ec473d025 Mon Sep 17 00:00:00 2001 From: Hikaru Saito Date: Fri, 6 Feb 2026 17:13:18 +0900 Subject: [PATCH 02/18] =?UTF-8?q?=E3=83=AA=E3=83=9D=E3=82=B8=E3=83=88?= =?UTF-8?q?=E3=83=AA=E3=81=AE=E3=83=A2=E3=83=83=E3=82=AF=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/repository/announcement_mock.go | 71 ++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 internal/repository/announcement_mock.go diff --git a/internal/repository/announcement_mock.go b/internal/repository/announcement_mock.go new file mode 100644 index 0000000..85bb253 --- /dev/null +++ b/internal/repository/announcement_mock.go @@ -0,0 +1,71 @@ +package repository + +import ( + "context" + "time" + + "github.com/fun-dotto/api-template/internal/domain" + "github.com/fun-dotto/api-template/internal/service" +) + +type mockAnnouncementRepository struct{} + +// NewMockAnnouncementRepository モックリポジトリを作成する +func NewMockAnnouncementRepository() service.AnnouncementRepository { + return &mockAnnouncementRepository{} +} + +// List 一覧を取得する(モック) +func (r *mockAnnouncementRepository) List(ctx context.Context) ([]domain.Announcement, error) { + now := time.Now() + until := now.Add(24 * time.Hour) + return []domain.Announcement{ + { + ID: "1", + Title: "お知らせ1", + URL: "https://example.com/1", + AvailableFrom: now, + AvailableUntil: &until, + }, + }, nil +} + +// Detail 詳細を取得する(モック) +func (r *mockAnnouncementRepository) Detail(ctx context.Context, id string) (*domain.Announcement, error) { + now := time.Now() + until := now.Add(24 * time.Hour) + return &domain.Announcement{ + ID: id, + Title: "お知らせ" + id, + URL: "https://example.com/" + id, + AvailableFrom: now, + AvailableUntil: &until, + }, nil +} + +// Create 新規作成する(モック) +func (r *mockAnnouncementRepository) Create(ctx context.Context, req *domain.AnnouncementRequest) (*domain.Announcement, error) { + return &domain.Announcement{ + ID: "created-id", + Title: req.Title, + URL: req.URL, + AvailableFrom: req.AvailableFrom, + AvailableUntil: req.AvailableUntil, + }, nil +} + +// Update 更新する(モック) +func (r *mockAnnouncementRepository) Update(ctx context.Context, id string, req *domain.AnnouncementRequest) (*domain.Announcement, error) { + return &domain.Announcement{ + ID: id, + Title: req.Title, + URL: req.URL, + AvailableFrom: req.AvailableFrom, + AvailableUntil: req.AvailableUntil, + }, nil +} + +// Delete 削除する(モック) +func (r *mockAnnouncementRepository) Delete(ctx context.Context, id string) error { + return nil +} From fbc2ef0ff52734bd382e439efa71aa232e3bb011 Mon Sep 17 00:00:00 2001 From: Hikaru Saito Date: Fri, 6 Feb 2026 17:13:36 +0900 Subject: [PATCH 03/18] =?UTF-8?q?Handler=E3=81=AEAnnouncement=E3=83=86?= =?UTF-8?q?=E3=82=B9=E3=83=88=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/handler/announcement_test.go | 387 ++++++++++++++++++++++++++ 1 file changed, 387 insertions(+) create mode 100644 internal/handler/announcement_test.go diff --git a/internal/handler/announcement_test.go b/internal/handler/announcement_test.go new file mode 100644 index 0000000..92d7c53 --- /dev/null +++ b/internal/handler/announcement_test.go @@ -0,0 +1,387 @@ +package handler_test + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "firebase.google.com/go/v4/auth" + api "github.com/fun-dotto/api-template/generated" + "github.com/fun-dotto/api-template/internal/handler" + "github.com/fun-dotto/api-template/internal/middleware" + "github.com/fun-dotto/api-template/internal/repository" + "github.com/fun-dotto/api-template/internal/service" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" +) + +// setupTestContext Firebase認証をモックしたテストコンテキストを作成する +func setupTestContext(withAdminClaim bool) (*httptest.ResponseRecorder, *gin.Context) { + gin.SetMode(gin.TestMode) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + // Requestを初期化 + c.Request = httptest.NewRequest(http.MethodGet, "/", nil) + + if withAdminClaim { + // Firebaseトークンをモック + token := &auth.Token{ + Claims: map[string]interface{}{ + "admin": true, + }, + } + c.Set(middleware.FirebaseTokenContextKey, token) + } + + return w, c +} + +func TestAnnouncementsV1List(t *testing.T) { + tests := []struct { + name string + withAdminClaim bool + wantCode int + validate func(t *testing.T, w *httptest.ResponseRecorder) + }{ + { + name: "正常にお知らせ一覧が取得できる", + withAdminClaim: true, + wantCode: http.StatusOK, + validate: func(t *testing.T, w *httptest.ResponseRecorder) { + var response map[string]interface{} + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err, "JSONのパースに失敗しました") + + announcements, ok := response["announcements"].([]interface{}) + assert.True(t, ok, "announcementsフィールドが配列ではありません") + assert.NotEmpty(t, announcements, "アナウンスメントが空です") + }, + }, + { + name: "Content-Typeがapplication/jsonである", + withAdminClaim: true, + wantCode: http.StatusOK, + validate: func(t *testing.T, w *httptest.ResponseRecorder) { + assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) + }, + }, + { + name: "レスポンスが正しい構造である", + withAdminClaim: true, + wantCode: http.StatusOK, + validate: func(t *testing.T, w *httptest.ResponseRecorder) { + var response map[string]interface{} + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + + announcements, ok := response["announcements"].([]interface{}) + assert.True(t, ok, "announcementsフィールドが存在しません") + assert.Len(t, announcements, 1, "MockRepositoryは1件返すはずです") + }, + }, + { + name: "お知らせのフィールドが正しく返される", + withAdminClaim: true, + wantCode: http.StatusOK, + validate: func(t *testing.T, w *httptest.ResponseRecorder) { + var response struct { + Announcements []api.Announcement `json:"announcements"` + } + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Len(t, response.Announcements, 1, "MockRepositoryは1件返すはずです") + assert.Equal(t, "1", response.Announcements[0].Id) + assert.Equal(t, "お知らせ1", response.Announcements[0].Title) + assert.Equal(t, "https://example.com/1", response.Announcements[0].Url) + assert.NotNil(t, response.Announcements[0].AvailableFrom) + assert.NotNil(t, response.Announcements[0].AvailableUntil) + }, + }, + { + name: "認証トークンがない場合は401エラー", + withAdminClaim: false, + wantCode: http.StatusUnauthorized, + validate: func(t *testing.T, w *httptest.ResponseRecorder) { + var response map[string]interface{} + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Contains(t, response["error"], "Authentication") + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockRepo := repository.NewMockAnnouncementRepository() + announcementService := service.NewAnnouncementService(mockRepo) + h := handler.NewHandler(announcementService) + w, c := setupTestContext(tt.withAdminClaim) + + h.AnnouncementsV1List(c) + + assert.Equal(t, tt.wantCode, w.Code) + + if tt.validate != nil { + tt.validate(t, w) + } + }) + } +} + +func TestAnnouncementsV1Detail(t *testing.T) { + tests := []struct { + name string + id string + withAdminClaim bool + wantCode int + validate func(t *testing.T, w *httptest.ResponseRecorder) + }{ + { + name: "正常にお知らせ詳細が取得できる", + id: "1", + withAdminClaim: true, + wantCode: http.StatusOK, + validate: func(t *testing.T, w *httptest.ResponseRecorder) { + var response struct { + Announcement api.Announcement `json:"announcement"` + } + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err, "JSONのパースに失敗しました") + assert.Equal(t, "1", response.Announcement.Id) + assert.Equal(t, "お知らせ1", response.Announcement.Title) + assert.Equal(t, "https://example.com/1", response.Announcement.Url) + }, + }, + { + name: "認証トークンがない場合は401エラー", + id: "1", + withAdminClaim: false, + wantCode: http.StatusUnauthorized, + validate: func(t *testing.T, w *httptest.ResponseRecorder) { + var response map[string]interface{} + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Contains(t, response["error"], "Authentication") + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockRepo := repository.NewMockAnnouncementRepository() + announcementService := service.NewAnnouncementService(mockRepo) + h := handler.NewHandler(announcementService) + w, c := setupTestContext(tt.withAdminClaim) + + h.AnnouncementsV1Detail(c, tt.id) + + assert.Equal(t, tt.wantCode, w.Code) + + if tt.validate != nil { + tt.validate(t, w) + } + }) + } +} + +func TestAnnouncementsV1Create(t *testing.T) { + now := time.Now() + until := now.Add(24 * time.Hour) + + tests := []struct { + name string + request api.AnnouncementRequest + withAdminClaim bool + wantCode int + validate func(t *testing.T, w *httptest.ResponseRecorder) + }{ + { + name: "正常にお知らせを作成できる", + request: api.AnnouncementRequest{ + Title: "新しいお知らせ", + Url: "https://example.com/new", + AvailableFrom: now, + AvailableUntil: &until, + }, + withAdminClaim: true, + wantCode: http.StatusOK, + validate: func(t *testing.T, w *httptest.ResponseRecorder) { + var response struct { + Announcement api.Announcement `json:"announcement"` + } + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err, "JSONのパースに失敗しました") + assert.Equal(t, "created-id", response.Announcement.Id) + assert.Equal(t, "新しいお知らせ", response.Announcement.Title) + assert.Equal(t, "https://example.com/new", response.Announcement.Url) + }, + }, + { + name: "認証トークンがない場合は401エラー", + request: api.AnnouncementRequest{ + Title: "新しいお知らせ", + Url: "https://example.com/new", + AvailableFrom: now, + AvailableUntil: &until, + }, + withAdminClaim: false, + wantCode: http.StatusUnauthorized, + validate: func(t *testing.T, w *httptest.ResponseRecorder) { + var response map[string]interface{} + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Contains(t, response["error"], "Authentication") + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockRepo := repository.NewMockAnnouncementRepository() + announcementService := service.NewAnnouncementService(mockRepo) + h := handler.NewHandler(announcementService) + w, c := setupTestContext(tt.withAdminClaim) + + // リクエストボディを設定 + body, _ := json.Marshal(tt.request) + c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/announcements", bytes.NewBuffer(body)) + c.Request.Header.Set("Content-Type", "application/json") + + h.AnnouncementsV1Create(c) + + assert.Equal(t, tt.wantCode, w.Code) + + if tt.validate != nil { + tt.validate(t, w) + } + }) + } +} + +func TestAnnouncementsV1Update(t *testing.T) { + now := time.Now() + until := now.Add(24 * time.Hour) + + tests := []struct { + name string + id string + request api.AnnouncementRequest + withAdminClaim bool + wantCode int + validate func(t *testing.T, w *httptest.ResponseRecorder) + }{ + { + name: "正常にお知らせを更新できる", + id: "1", + request: api.AnnouncementRequest{ + Title: "更新されたお知らせ", + Url: "https://example.com/updated", + AvailableFrom: now, + AvailableUntil: &until, + }, + withAdminClaim: true, + wantCode: http.StatusOK, + validate: func(t *testing.T, w *httptest.ResponseRecorder) { + var response struct { + Announcement api.Announcement `json:"announcement"` + } + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err, "JSONのパースに失敗しました") + assert.Equal(t, "1", response.Announcement.Id) + assert.Equal(t, "更新されたお知らせ", response.Announcement.Title) + assert.Equal(t, "https://example.com/updated", response.Announcement.Url) + }, + }, + { + name: "認証トークンがない場合は401エラー", + id: "1", + request: api.AnnouncementRequest{ + Title: "更新されたお知らせ", + Url: "https://example.com/updated", + AvailableFrom: now, + AvailableUntil: &until, + }, + withAdminClaim: false, + wantCode: http.StatusUnauthorized, + validate: func(t *testing.T, w *httptest.ResponseRecorder) { + var response map[string]interface{} + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Contains(t, response["error"], "Authentication") + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockRepo := repository.NewMockAnnouncementRepository() + announcementService := service.NewAnnouncementService(mockRepo) + h := handler.NewHandler(announcementService) + w, c := setupTestContext(tt.withAdminClaim) + + // リクエストボディを設定 + body, _ := json.Marshal(tt.request) + c.Request = httptest.NewRequest(http.MethodPut, "/api/v1/announcements/"+tt.id, bytes.NewBuffer(body)) + c.Request.Header.Set("Content-Type", "application/json") + + h.AnnouncementsV1Update(c, tt.id) + + assert.Equal(t, tt.wantCode, w.Code) + + if tt.validate != nil { + tt.validate(t, w) + } + }) + } +} + +func TestAnnouncementsV1Delete(t *testing.T) { + tests := []struct { + name string + id string + withAdminClaim bool + wantCode int + validate func(t *testing.T, w *httptest.ResponseRecorder) + }{ + { + name: "正常にお知らせを削除できる", + id: "1", + withAdminClaim: true, + wantCode: http.StatusOK, + validate: nil, + }, + { + name: "認証トークンがない場合は401エラー", + id: "1", + withAdminClaim: false, + wantCode: http.StatusUnauthorized, + validate: func(t *testing.T, w *httptest.ResponseRecorder) { + var response map[string]interface{} + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Contains(t, response["error"], "Authentication") + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockRepo := repository.NewMockAnnouncementRepository() + announcementService := service.NewAnnouncementService(mockRepo) + h := handler.NewHandler(announcementService) + w, c := setupTestContext(tt.withAdminClaim) + + h.AnnouncementsV1Delete(c, tt.id) + + assert.Equal(t, tt.wantCode, w.Code) + + if tt.validate != nil { + tt.validate(t, w) + } + }) + } +} From 74836b204355f4b47dd7494ce5e5434bbc345d8d Mon Sep 17 00:00:00 2001 From: Hikaru Saito Date: Sun, 8 Feb 2026 15:09:02 +0900 Subject: [PATCH 04/18] =?UTF-8?q?Announcement=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=81=AB=E3=82=AB=E3=82=B9=E3=82=BF=E3=83=A0=E3=82=AF=E3=83=AC?= =?UTF-8?q?=E3=83=BC=E3=83=A0=E3=82=92=E4=BD=BF=E7=94=A8=E3=81=97=E3=81=9F?= =?UTF-8?q?403=E3=82=A8=E3=83=A9=E3=83=BC=E6=A4=9C=E8=A8=BC=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/handler/announcement_test.go | 37 ++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/internal/handler/announcement_test.go b/internal/handler/announcement_test.go index 92d7c53..1c521d6 100644 --- a/internal/handler/announcement_test.go +++ b/internal/handler/announcement_test.go @@ -40,6 +40,17 @@ func setupTestContext(withAdminClaim bool) (*httptest.ResponseRecorder, *gin.Con return w, c } +// setupTestContextWithClaims 指定したクレームでFirebaseトークンをモックしたテストコンテキストを作成する(403など権限不足の検証用) +func setupTestContextWithClaims(claims map[string]interface{}) (*httptest.ResponseRecorder, *gin.Context) { + gin.SetMode(gin.TestMode) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(http.MethodGet, "/", nil) + token := &auth.Token{Claims: claims} + c.Set(middleware.FirebaseTokenContextKey, token) + return w, c +} + func TestAnnouncementsV1List(t *testing.T) { tests := []struct { name string @@ -196,6 +207,7 @@ func TestAnnouncementsV1Create(t *testing.T) { name string request api.AnnouncementRequest withAdminClaim bool + customClaims map[string]interface{} // 指定時はこのクレームでトークンをセット(403検証用) wantCode int validate func(t *testing.T, w *httptest.ResponseRecorder) }{ @@ -237,6 +249,23 @@ func TestAnnouncementsV1Create(t *testing.T) { assert.Contains(t, response["error"], "Authentication") }, }, + { + name: "admin/developer以外のクレームのみのトークンでは403エラー", + request: api.AnnouncementRequest{ + Title: "新しいお知らせ", + Url: "https://example.com/new", + AvailableFrom: now, + AvailableUntil: &until, + }, + customClaims: map[string]interface{}{"user": true}, + wantCode: http.StatusForbidden, + validate: func(t *testing.T, w *httptest.ResponseRecorder) { + var response map[string]interface{} + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, "Insufficient permissions", response["error"]) + }, + }, } for _, tt := range tests { @@ -244,7 +273,13 @@ func TestAnnouncementsV1Create(t *testing.T) { mockRepo := repository.NewMockAnnouncementRepository() announcementService := service.NewAnnouncementService(mockRepo) h := handler.NewHandler(announcementService) - w, c := setupTestContext(tt.withAdminClaim) + var w *httptest.ResponseRecorder + var c *gin.Context + if tt.customClaims != nil { + w, c = setupTestContextWithClaims(tt.customClaims) + } else { + w, c = setupTestContext(tt.withAdminClaim) + } // リクエストボディを設定 body, _ := json.Marshal(tt.request) From 3a630f6150f8bb33f638b08abbc3b3bd438843a6 Mon Sep 17 00:00:00 2001 From: Hikaru Saito Date: Sun, 8 Feb 2026 15:10:19 +0900 Subject: [PATCH 05/18] Update authentication error assertions in announcement tests to check for exact error message --- internal/handler/announcement_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/handler/announcement_test.go b/internal/handler/announcement_test.go index 1c521d6..ff4a42a 100644 --- a/internal/handler/announcement_test.go +++ b/internal/handler/announcement_test.go @@ -120,7 +120,7 @@ func TestAnnouncementsV1List(t *testing.T) { var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) - assert.Contains(t, response["error"], "Authentication") + assert.Equal(t, "Authentication required", response["error"]) }, }, } @@ -176,7 +176,7 @@ func TestAnnouncementsV1Detail(t *testing.T) { var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) - assert.Contains(t, response["error"], "Authentication") + assert.Equal(t, "Authentication required", response["error"]) }, }, } @@ -246,7 +246,7 @@ func TestAnnouncementsV1Create(t *testing.T) { var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) - assert.Contains(t, response["error"], "Authentication") + assert.Equal(t, "Authentication required", response["error"]) }, }, { @@ -346,7 +346,7 @@ func TestAnnouncementsV1Update(t *testing.T) { var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) - assert.Contains(t, response["error"], "Authentication") + assert.Equal(t, "Authentication required", response["error"]) }, }, } @@ -398,7 +398,7 @@ func TestAnnouncementsV1Delete(t *testing.T) { var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) - assert.Contains(t, response["error"], "Authentication") + assert.Equal(t, "Authentication required", response["error"]) }, }, } From 54ab59a79da671a949cbf3f793a4bae698a47560 Mon Sep 17 00:00:00 2001 From: Hikaru Saito Date: Sun, 8 Feb 2026 15:11:46 +0900 Subject: [PATCH 06/18] Add developer claim support in announcement list tests Enhanced the TestAnnouncementsV1List function to include a new test case for handling requests with a developer claim. Updated the test structure to accommodate the additional claim and ensured proper validation of the response for this scenario. --- internal/handler/announcement_test.go | 31 ++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/internal/handler/announcement_test.go b/internal/handler/announcement_test.go index ff4a42a..c837611 100644 --- a/internal/handler/announcement_test.go +++ b/internal/handler/announcement_test.go @@ -53,10 +53,11 @@ func setupTestContextWithClaims(claims map[string]interface{}) (*httptest.Respon func TestAnnouncementsV1List(t *testing.T) { tests := []struct { - name string - withAdminClaim bool - wantCode int - validate func(t *testing.T, w *httptest.ResponseRecorder) + name string + withAdminClaim bool + withDeveloperClaim bool + wantCode int + validate func(t *testing.T, w *httptest.ResponseRecorder) }{ { name: "正常にお知らせ一覧が取得できる", @@ -72,6 +73,20 @@ func TestAnnouncementsV1List(t *testing.T) { assert.NotEmpty(t, announcements, "アナウンスメントが空です") }, }, + { + name: "developerクレームのみでも一覧が取得できる", + withDeveloperClaim: true, + wantCode: http.StatusOK, + validate: func(t *testing.T, w *httptest.ResponseRecorder) { + var response map[string]interface{} + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err, "JSONのパースに失敗しました") + announcements, ok := response["announcements"].([]interface{}) + assert.True(t, ok, "announcementsフィールドが配列ではありません") + assert.NotEmpty(t, announcements, "アナウンスメントが空です") + assert.Len(t, announcements, 1, "MockRepositoryは1件返すはずです") + }, + }, { name: "Content-Typeがapplication/jsonである", withAdminClaim: true, @@ -130,7 +145,13 @@ func TestAnnouncementsV1List(t *testing.T) { mockRepo := repository.NewMockAnnouncementRepository() announcementService := service.NewAnnouncementService(mockRepo) h := handler.NewHandler(announcementService) - w, c := setupTestContext(tt.withAdminClaim) + var w *httptest.ResponseRecorder + var c *gin.Context + if tt.withDeveloperClaim { + w, c = setupTestContextWithClaims(map[string]interface{}{"developer": true}) + } else { + w, c = setupTestContext(tt.withAdminClaim) + } h.AnnouncementsV1List(c) From d9bea5da37f6a98897e45d68257013ec795e2a3b Mon Sep 17 00:00:00 2001 From: Hikaru Saito Date: Sun, 8 Feb 2026 15:15:26 +0900 Subject: [PATCH 07/18] Add custom claims test case for 403 error in announcement update handler Enhanced the TestAnnouncementsV1Update function by adding a new test case that verifies the response when a token with non-admin/developer claims is used. This ensures proper handling of insufficient permissions and improves overall test coverage for authentication scenarios. --- internal/handler/announcement_test.go | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/internal/handler/announcement_test.go b/internal/handler/announcement_test.go index c837611..d67b08b 100644 --- a/internal/handler/announcement_test.go +++ b/internal/handler/announcement_test.go @@ -327,6 +327,7 @@ func TestAnnouncementsV1Update(t *testing.T) { id string request api.AnnouncementRequest withAdminClaim bool + customClaims map[string]interface{} // 指定時はこのクレームでトークンをセット(403検証用) wantCode int validate func(t *testing.T, w *httptest.ResponseRecorder) }{ @@ -370,6 +371,24 @@ func TestAnnouncementsV1Update(t *testing.T) { assert.Equal(t, "Authentication required", response["error"]) }, }, + { + name: "admin/developer以外のクレームのみのトークンでは403エラー", + id: "1", + request: api.AnnouncementRequest{ + Title: "更新されたお知らせ", + Url: "https://example.com/updated", + AvailableFrom: now, + AvailableUntil: &until, + }, + customClaims: map[string]interface{}{"user": true}, + wantCode: http.StatusForbidden, + validate: func(t *testing.T, w *httptest.ResponseRecorder) { + var response map[string]interface{} + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, "Insufficient permissions", response["error"]) + }, + }, } for _, tt := range tests { @@ -377,7 +396,13 @@ func TestAnnouncementsV1Update(t *testing.T) { mockRepo := repository.NewMockAnnouncementRepository() announcementService := service.NewAnnouncementService(mockRepo) h := handler.NewHandler(announcementService) - w, c := setupTestContext(tt.withAdminClaim) + var w *httptest.ResponseRecorder + var c *gin.Context + if tt.customClaims != nil { + w, c = setupTestContextWithClaims(tt.customClaims) + } else { + w, c = setupTestContext(tt.withAdminClaim) + } // リクエストボディを設定 body, _ := json.Marshal(tt.request) From 20ec1666eab0ddde8a20c76957b2b8c5192f394a Mon Sep 17 00:00:00 2001 From: Hikaru Saito Date: Sun, 8 Feb 2026 15:17:56 +0900 Subject: [PATCH 08/18] Add developer claim support in announcement creation tests Enhanced the TestAnnouncementsV1Create function by adding a new test case that verifies the ability to create announcements with a developer claim. Updated the test structure to include the new claim and ensured proper validation of the response for this scenario. --- internal/handler/announcement_test.go | 36 ++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/internal/handler/announcement_test.go b/internal/handler/announcement_test.go index d67b08b..ed20e9d 100644 --- a/internal/handler/announcement_test.go +++ b/internal/handler/announcement_test.go @@ -225,12 +225,13 @@ func TestAnnouncementsV1Create(t *testing.T) { until := now.Add(24 * time.Hour) tests := []struct { - name string - request api.AnnouncementRequest - withAdminClaim bool - customClaims map[string]interface{} // 指定時はこのクレームでトークンをセット(403検証用) - wantCode int - validate func(t *testing.T, w *httptest.ResponseRecorder) + name string + request api.AnnouncementRequest + withAdminClaim bool + withDeveloperClaim bool + customClaims map[string]interface{} // 指定時はこのクレームでトークンをセット(403検証用) + wantCode int + validate func(t *testing.T, w *httptest.ResponseRecorder) }{ { name: "正常にお知らせを作成できる", @@ -253,6 +254,27 @@ func TestAnnouncementsV1Create(t *testing.T) { assert.Equal(t, "https://example.com/new", response.Announcement.Url) }, }, + { + name: "developerクレームのみでも作成できる", + request: api.AnnouncementRequest{ + Title: "developer経由のお知らせ", + Url: "https://example.com/developer", + AvailableFrom: now, + AvailableUntil: &until, + }, + withDeveloperClaim: true, + wantCode: http.StatusOK, + validate: func(t *testing.T, w *httptest.ResponseRecorder) { + var response struct { + Announcement api.Announcement `json:"announcement"` + } + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err, "JSONのパースに失敗しました") + assert.Equal(t, "created-id", response.Announcement.Id) + assert.Equal(t, "developer経由のお知らせ", response.Announcement.Title) + assert.Equal(t, "https://example.com/developer", response.Announcement.Url) + }, + }, { name: "認証トークンがない場合は401エラー", request: api.AnnouncementRequest{ @@ -298,6 +320,8 @@ func TestAnnouncementsV1Create(t *testing.T) { var c *gin.Context if tt.customClaims != nil { w, c = setupTestContextWithClaims(tt.customClaims) + } else if tt.withDeveloperClaim { + w, c = setupTestContextWithClaims(map[string]interface{}{"developer": true}) } else { w, c = setupTestContext(tt.withAdminClaim) } From d1e5c8ca12eb4ab2b07995f33593b08968ae74c3 Mon Sep 17 00:00:00 2001 From: Hikaru Saito Date: Sun, 8 Feb 2026 15:20:08 +0900 Subject: [PATCH 09/18] Enhance announcement deletion tests to support developer claims Updated the TestAnnouncementsV1Delete function to include a new test case for handling deletion requests with a developer claim. Adjusted the expected status code to reflect the correct response for successful deletions and modified the test setup to accommodate different claim types. Additionally, changed the handler to use AbortWithStatus for consistency in response handling. --- internal/handler/announcement.go | 2 +- internal/handler/announcement_test.go | 28 ++++++++++++++++++++------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/internal/handler/announcement.go b/internal/handler/announcement.go index 8d78ab1..d9ae683 100644 --- a/internal/handler/announcement.go +++ b/internal/handler/announcement.go @@ -77,7 +77,7 @@ func (h *Handler) AnnouncementsV1Delete(c *gin.Context, id string) { return } - c.Status(http.StatusNoContent) + c.AbortWithStatus(http.StatusNoContent) } // AnnouncementsV1Update 更新する diff --git a/internal/handler/announcement_test.go b/internal/handler/announcement_test.go index ed20e9d..9bc91d2 100644 --- a/internal/handler/announcement_test.go +++ b/internal/handler/announcement_test.go @@ -446,19 +446,27 @@ func TestAnnouncementsV1Update(t *testing.T) { func TestAnnouncementsV1Delete(t *testing.T) { tests := []struct { - name string - id string - withAdminClaim bool - wantCode int - validate func(t *testing.T, w *httptest.ResponseRecorder) + name string + id string + withAdminClaim bool + withDeveloperClaim bool + wantCode int + validate func(t *testing.T, w *httptest.ResponseRecorder) }{ { name: "正常にお知らせを削除できる", id: "1", withAdminClaim: true, - wantCode: http.StatusOK, + wantCode: http.StatusNoContent, validate: nil, }, + { + name: "developerクレームのみでも削除できる", + id: "1", + withDeveloperClaim: true, + wantCode: http.StatusNoContent, + validate: nil, + }, { name: "認証トークンがない場合は401エラー", id: "1", @@ -478,7 +486,13 @@ func TestAnnouncementsV1Delete(t *testing.T) { mockRepo := repository.NewMockAnnouncementRepository() announcementService := service.NewAnnouncementService(mockRepo) h := handler.NewHandler(announcementService) - w, c := setupTestContext(tt.withAdminClaim) + var w *httptest.ResponseRecorder + var c *gin.Context + if tt.withDeveloperClaim { + w, c = setupTestContextWithClaims(map[string]interface{}{"developer": true}) + } else { + w, c = setupTestContext(tt.withAdminClaim) + } h.AnnouncementsV1Delete(c, tt.id) From 88ed46eb500ea3ee5276779929aa95c9518898d9 Mon Sep 17 00:00:00 2001 From: Hikaru Saito Date: Sun, 8 Feb 2026 15:21:31 +0900 Subject: [PATCH 10/18] Refactor announcement tests to improve error handling and structure Updated the TestAnnouncementsV1List and TestAnnouncementsV1Update functions to enhance test clarity and error handling. Introduced the require package for better error assertions and adjusted variable formatting for consistency. This improves overall test reliability and readability. --- internal/handler/announcement_test.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/internal/handler/announcement_test.go b/internal/handler/announcement_test.go index 9bc91d2..2563d40 100644 --- a/internal/handler/announcement_test.go +++ b/internal/handler/announcement_test.go @@ -16,6 +16,7 @@ import ( "github.com/fun-dotto/api-template/internal/service" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // setupTestContext Firebase認証をモックしたテストコンテキストを作成する @@ -53,11 +54,11 @@ func setupTestContextWithClaims(claims map[string]interface{}) (*httptest.Respon func TestAnnouncementsV1List(t *testing.T) { tests := []struct { - name string - withAdminClaim bool + name string + withAdminClaim bool withDeveloperClaim bool - wantCode int - validate func(t *testing.T, w *httptest.ResponseRecorder) + wantCode int + validate func(t *testing.T, w *httptest.ResponseRecorder) }{ { name: "正常にお知らせ一覧が取得できる", @@ -429,7 +430,8 @@ func TestAnnouncementsV1Update(t *testing.T) { } // リクエストボディを設定 - body, _ := json.Marshal(tt.request) + body, err := json.Marshal(tt.request) + require.NoError(t, err, "リクエストボディのJSONエンコードに失敗しました") c.Request = httptest.NewRequest(http.MethodPut, "/api/v1/announcements/"+tt.id, bytes.NewBuffer(body)) c.Request.Header.Set("Content-Type", "application/json") From bdc58d5d62925ea501a63fc649e27e328ef63438 Mon Sep 17 00:00:00 2001 From: Hikaru Saito Date: Sun, 8 Feb 2026 16:50:06 +0900 Subject: [PATCH 11/18] =?UTF-8?q?Create=E6=99=82=E3=81=AE=E3=82=B9?= =?UTF-8?q?=E3=83=86=E3=83=BC=E3=82=BF=E3=82=B9=E3=82=B3=E3=83=BC=E3=83=89?= =?UTF-8?q?=E3=82=92201=E3=81=AB=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/handler/announcement.go | 2 +- internal/handler/announcement_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/handler/announcement.go b/internal/handler/announcement.go index d9ae683..b3611c2 100644 --- a/internal/handler/announcement.go +++ b/internal/handler/announcement.go @@ -61,7 +61,7 @@ func (h *Handler) AnnouncementsV1Create(c *gin.Context) { return } - c.JSON(http.StatusOK, gin.H{ + c.JSON(http.StatusCreated, gin.H{ "announcement": ToAPIAnnouncement(announcement), }) } diff --git a/internal/handler/announcement_test.go b/internal/handler/announcement_test.go index 2563d40..f28e3d3 100644 --- a/internal/handler/announcement_test.go +++ b/internal/handler/announcement_test.go @@ -243,7 +243,7 @@ func TestAnnouncementsV1Create(t *testing.T) { AvailableUntil: &until, }, withAdminClaim: true, - wantCode: http.StatusOK, + wantCode: http.StatusCreated, validate: func(t *testing.T, w *httptest.ResponseRecorder) { var response struct { Announcement api.Announcement `json:"announcement"` @@ -264,7 +264,7 @@ func TestAnnouncementsV1Create(t *testing.T) { AvailableUntil: &until, }, withDeveloperClaim: true, - wantCode: http.StatusOK, + wantCode: http.StatusCreated, validate: func(t *testing.T, w *httptest.ResponseRecorder) { var response struct { Announcement api.Announcement `json:"announcement"` From 7d82fce98fb2c55bf6027add3f4a9cfff515d993 Mon Sep 17 00:00:00 2001 From: Hikaru Saito Date: Sun, 8 Feb 2026 16:51:06 +0900 Subject: [PATCH 12/18] =?UTF-8?q?Abort=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/handler/announcement.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/handler/announcement.go b/internal/handler/announcement.go index b3611c2..273d7eb 100644 --- a/internal/handler/announcement.go +++ b/internal/handler/announcement.go @@ -77,7 +77,7 @@ func (h *Handler) AnnouncementsV1Delete(c *gin.Context, id string) { return } - c.AbortWithStatus(http.StatusNoContent) + c.Status(http.StatusNoContent) } // AnnouncementsV1Update 更新する From 29f980b7b9c4117cbf5501cd65ad53186cfbd595 Mon Sep 17 00:00:00 2001 From: Hikaru Saito Date: Sun, 8 Feb 2026 16:51:54 +0900 Subject: [PATCH 13/18] =?UTF-8?q?=E3=82=A8=E3=83=A9=E3=83=BC=E3=82=92?= =?UTF-8?q?=E6=98=8E=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/handler/announcement_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/handler/announcement_test.go b/internal/handler/announcement_test.go index f28e3d3..4be6cb1 100644 --- a/internal/handler/announcement_test.go +++ b/internal/handler/announcement_test.go @@ -328,7 +328,8 @@ func TestAnnouncementsV1Create(t *testing.T) { } // リクエストボディを設定 - body, _ := json.Marshal(tt.request) + body, err := json.Marshal(tt.request) + require.NoError(t, err, "リクエストボディのJSONエンコードに失敗しました") c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/announcements", bytes.NewBuffer(body)) c.Request.Header.Set("Content-Type", "application/json") From 25add9c73010c1d7c5efcf30bcd24991eefc1232 Mon Sep 17 00:00:00 2001 From: Hikaru Saito Date: Sun, 8 Feb 2026 17:09:43 +0900 Subject: [PATCH 14/18] Add WriteHeaderNow call in AnnouncementsV1Delete handler for immediate response --- internal/handler/announcement.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/handler/announcement.go b/internal/handler/announcement.go index 273d7eb..10a2921 100644 --- a/internal/handler/announcement.go +++ b/internal/handler/announcement.go @@ -78,6 +78,7 @@ func (h *Handler) AnnouncementsV1Delete(c *gin.Context, id string) { } c.Status(http.StatusNoContent) + c.Writer.WriteHeaderNow() } // AnnouncementsV1Update 更新する From 982fdb3fa0a3a570bb8253b700a573faab2a458c Mon Sep 17 00:00:00 2001 From: Hikaru Saito <135803012+hikaru-0602@users.noreply.github.com> Date: Tue, 10 Feb 2026 11:06:02 +0900 Subject: [PATCH 15/18] Update internal/handler/announcement_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- internal/handler/announcement_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/handler/announcement_test.go b/internal/handler/announcement_test.go index 4be6cb1..a1edac5 100644 --- a/internal/handler/announcement_test.go +++ b/internal/handler/announcement_test.go @@ -106,7 +106,7 @@ func TestAnnouncementsV1List(t *testing.T) { assert.NoError(t, err) announcements, ok := response["announcements"].([]interface{}) - assert.True(t, ok, "announcementsフィールドが存在しません") + assert.True(t, ok, "announcementsフィールドが配列ではありません") assert.Len(t, announcements, 1, "MockRepositoryは1件返すはずです") }, }, From a20f047d2e831c27a8eb12a6bf523422448ba11c Mon Sep 17 00:00:00 2001 From: Hikaru Saito Date: Tue, 10 Feb 2026 11:19:37 +0900 Subject: [PATCH 16/18] =?UTF-8?q?=E5=9E=8B=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/handler/announcement_test.go | 28 +++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/internal/handler/announcement_test.go b/internal/handler/announcement_test.go index a1edac5..4a7d7e8 100644 --- a/internal/handler/announcement_test.go +++ b/internal/handler/announcement_test.go @@ -116,7 +116,7 @@ func TestAnnouncementsV1List(t *testing.T) { wantCode: http.StatusOK, validate: func(t *testing.T, w *httptest.ResponseRecorder) { var response struct { - Announcements []api.Announcement `json:"announcements"` + Announcements []api.AnnouncementServiceAnnouncement `json:"announcements"` } err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) @@ -180,7 +180,7 @@ func TestAnnouncementsV1Detail(t *testing.T) { wantCode: http.StatusOK, validate: func(t *testing.T, w *httptest.ResponseRecorder) { var response struct { - Announcement api.Announcement `json:"announcement"` + Announcement api.AnnouncementServiceAnnouncement `json:"announcement"` } err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err, "JSONのパースに失敗しました") @@ -227,7 +227,7 @@ func TestAnnouncementsV1Create(t *testing.T) { tests := []struct { name string - request api.AnnouncementRequest + request api.AnnouncementServiceAnnouncementRequest withAdminClaim bool withDeveloperClaim bool customClaims map[string]interface{} // 指定時はこのクレームでトークンをセット(403検証用) @@ -236,7 +236,7 @@ func TestAnnouncementsV1Create(t *testing.T) { }{ { name: "正常にお知らせを作成できる", - request: api.AnnouncementRequest{ + request: api.AnnouncementServiceAnnouncementRequest{ Title: "新しいお知らせ", Url: "https://example.com/new", AvailableFrom: now, @@ -246,7 +246,7 @@ func TestAnnouncementsV1Create(t *testing.T) { wantCode: http.StatusCreated, validate: func(t *testing.T, w *httptest.ResponseRecorder) { var response struct { - Announcement api.Announcement `json:"announcement"` + Announcement api.AnnouncementServiceAnnouncement `json:"announcement"` } err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err, "JSONのパースに失敗しました") @@ -257,7 +257,7 @@ func TestAnnouncementsV1Create(t *testing.T) { }, { name: "developerクレームのみでも作成できる", - request: api.AnnouncementRequest{ + request: api.AnnouncementServiceAnnouncementRequest{ Title: "developer経由のお知らせ", Url: "https://example.com/developer", AvailableFrom: now, @@ -267,7 +267,7 @@ func TestAnnouncementsV1Create(t *testing.T) { wantCode: http.StatusCreated, validate: func(t *testing.T, w *httptest.ResponseRecorder) { var response struct { - Announcement api.Announcement `json:"announcement"` + Announcement api.AnnouncementServiceAnnouncement `json:"announcement"` } err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err, "JSONのパースに失敗しました") @@ -278,7 +278,7 @@ func TestAnnouncementsV1Create(t *testing.T) { }, { name: "認証トークンがない場合は401エラー", - request: api.AnnouncementRequest{ + request: api.AnnouncementServiceAnnouncementRequest{ Title: "新しいお知らせ", Url: "https://example.com/new", AvailableFrom: now, @@ -295,7 +295,7 @@ func TestAnnouncementsV1Create(t *testing.T) { }, { name: "admin/developer以外のクレームのみのトークンでは403エラー", - request: api.AnnouncementRequest{ + request: api.AnnouncementServiceAnnouncementRequest{ Title: "新しいお知らせ", Url: "https://example.com/new", AvailableFrom: now, @@ -351,7 +351,7 @@ func TestAnnouncementsV1Update(t *testing.T) { tests := []struct { name string id string - request api.AnnouncementRequest + request api.AnnouncementServiceAnnouncementRequest withAdminClaim bool customClaims map[string]interface{} // 指定時はこのクレームでトークンをセット(403検証用) wantCode int @@ -360,7 +360,7 @@ func TestAnnouncementsV1Update(t *testing.T) { { name: "正常にお知らせを更新できる", id: "1", - request: api.AnnouncementRequest{ + request: api.AnnouncementServiceAnnouncementRequest{ Title: "更新されたお知らせ", Url: "https://example.com/updated", AvailableFrom: now, @@ -370,7 +370,7 @@ func TestAnnouncementsV1Update(t *testing.T) { wantCode: http.StatusOK, validate: func(t *testing.T, w *httptest.ResponseRecorder) { var response struct { - Announcement api.Announcement `json:"announcement"` + Announcement api.AnnouncementServiceAnnouncement `json:"announcement"` } err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err, "JSONのパースに失敗しました") @@ -382,7 +382,7 @@ func TestAnnouncementsV1Update(t *testing.T) { { name: "認証トークンがない場合は401エラー", id: "1", - request: api.AnnouncementRequest{ + request: api.AnnouncementServiceAnnouncementRequest{ Title: "更新されたお知らせ", Url: "https://example.com/updated", AvailableFrom: now, @@ -400,7 +400,7 @@ func TestAnnouncementsV1Update(t *testing.T) { { name: "admin/developer以外のクレームのみのトークンでは403エラー", id: "1", - request: api.AnnouncementRequest{ + request: api.AnnouncementServiceAnnouncementRequest{ Title: "更新されたお知らせ", Url: "https://example.com/updated", AvailableFrom: now, From 38d47ecf25fc9ab510808e437783995e33b4d300 Mon Sep 17 00:00:00 2001 From: Hikaru Saito Date: Tue, 10 Feb 2026 11:21:59 +0900 Subject: [PATCH 17/18] Add custom claims support in announcement tests for enhanced permission validation Updated TestAnnouncementsV1List, TestAnnouncementsV1Detail, and TestAnnouncementsV1Delete to include custom claims for testing 403 error scenarios. This allows for more granular permission checks and improves test coverage for authorization logic. --- internal/handler/announcement_test.go | 54 +++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/internal/handler/announcement_test.go b/internal/handler/announcement_test.go index 4a7d7e8..0dbea05 100644 --- a/internal/handler/announcement_test.go +++ b/internal/handler/announcement_test.go @@ -57,6 +57,7 @@ func TestAnnouncementsV1List(t *testing.T) { name string withAdminClaim bool withDeveloperClaim bool + customClaims map[string]interface{} // 指定時はこのクレームでトークンをセット(403検証用) wantCode int validate func(t *testing.T, w *httptest.ResponseRecorder) }{ @@ -139,6 +140,17 @@ func TestAnnouncementsV1List(t *testing.T) { assert.Equal(t, "Authentication required", response["error"]) }, }, + { + name: "admin/developer以外のクレームのみのトークンでは403エラー", + customClaims: map[string]interface{}{"user": true}, + wantCode: http.StatusForbidden, + validate: func(t *testing.T, w *httptest.ResponseRecorder) { + var response map[string]interface{} + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, "Insufficient permissions", response["error"]) + }, + }, } for _, tt := range tests { @@ -148,7 +160,9 @@ func TestAnnouncementsV1List(t *testing.T) { h := handler.NewHandler(announcementService) var w *httptest.ResponseRecorder var c *gin.Context - if tt.withDeveloperClaim { + if tt.customClaims != nil { + w, c = setupTestContextWithClaims(tt.customClaims) + } else if tt.withDeveloperClaim { w, c = setupTestContextWithClaims(map[string]interface{}{"developer": true}) } else { w, c = setupTestContext(tt.withAdminClaim) @@ -170,6 +184,7 @@ func TestAnnouncementsV1Detail(t *testing.T) { name string id string withAdminClaim bool + customClaims map[string]interface{} // 指定時はこのクレームでトークンをセット(403検証用) wantCode int validate func(t *testing.T, w *httptest.ResponseRecorder) }{ @@ -201,6 +216,18 @@ func TestAnnouncementsV1Detail(t *testing.T) { assert.Equal(t, "Authentication required", response["error"]) }, }, + { + name: "admin/developer以外のクレームのみのトークンでは403エラー", + id: "1", + customClaims: map[string]interface{}{"user": true}, + wantCode: http.StatusForbidden, + validate: func(t *testing.T, w *httptest.ResponseRecorder) { + var response map[string]interface{} + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, "Insufficient permissions", response["error"]) + }, + }, } for _, tt := range tests { @@ -208,7 +235,13 @@ func TestAnnouncementsV1Detail(t *testing.T) { mockRepo := repository.NewMockAnnouncementRepository() announcementService := service.NewAnnouncementService(mockRepo) h := handler.NewHandler(announcementService) - w, c := setupTestContext(tt.withAdminClaim) + var w *httptest.ResponseRecorder + var c *gin.Context + if tt.customClaims != nil { + w, c = setupTestContextWithClaims(tt.customClaims) + } else { + w, c = setupTestContext(tt.withAdminClaim) + } h.AnnouncementsV1Detail(c, tt.id) @@ -453,6 +486,7 @@ func TestAnnouncementsV1Delete(t *testing.T) { id string withAdminClaim bool withDeveloperClaim bool + customClaims map[string]interface{} // 指定時はこのクレームでトークンをセット(403検証用) wantCode int validate func(t *testing.T, w *httptest.ResponseRecorder) }{ @@ -482,6 +516,18 @@ func TestAnnouncementsV1Delete(t *testing.T) { assert.Equal(t, "Authentication required", response["error"]) }, }, + { + name: "admin/developer以外のクレームのみのトークンでは403エラー", + id: "1", + customClaims: map[string]interface{}{"user": true}, + wantCode: http.StatusForbidden, + validate: func(t *testing.T, w *httptest.ResponseRecorder) { + var response map[string]interface{} + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, "Insufficient permissions", response["error"]) + }, + }, } for _, tt := range tests { @@ -491,7 +537,9 @@ func TestAnnouncementsV1Delete(t *testing.T) { h := handler.NewHandler(announcementService) var w *httptest.ResponseRecorder var c *gin.Context - if tt.withDeveloperClaim { + if tt.customClaims != nil { + w, c = setupTestContextWithClaims(tt.customClaims) + } else if tt.withDeveloperClaim { w, c = setupTestContextWithClaims(map[string]interface{}{"developer": true}) } else { w, c = setupTestContext(tt.withAdminClaim) From b39ab96c64f001e3cd9e2475cc304b5cb466f255 Mon Sep 17 00:00:00 2001 From: Hikaru Saito Date: Tue, 10 Feb 2026 11:23:41 +0900 Subject: [PATCH 18/18] =?UTF-8?q?=E3=82=BC=E3=83=AD=E3=81=A7=E3=81=AF?= =?UTF-8?q?=E7=84=A1=E3=81=84=E3=81=93=E3=81=A8=E3=82=92=E6=A4=9C=E8=A8=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/handler/announcement_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/handler/announcement_test.go b/internal/handler/announcement_test.go index 0dbea05..4ceefba 100644 --- a/internal/handler/announcement_test.go +++ b/internal/handler/announcement_test.go @@ -125,8 +125,8 @@ func TestAnnouncementsV1List(t *testing.T) { assert.Equal(t, "1", response.Announcements[0].Id) assert.Equal(t, "お知らせ1", response.Announcements[0].Title) assert.Equal(t, "https://example.com/1", response.Announcements[0].Url) - assert.NotNil(t, response.Announcements[0].AvailableFrom) - assert.NotNil(t, response.Announcements[0].AvailableUntil) + assert.False(t, response.Announcements[0].AvailableFrom.IsZero(), "AvailableFromが設定されていること") + assert.False(t, response.Announcements[0].AvailableUntil.IsZero(), "AvailableUntilが設定されていること") }, }, {