Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func main() {
log.Fatalf("Failed to initialize external clients: %v", err)
}

h := handler.NewHandler(clients.Academic, clients.Announcement)
h := handler.NewHandler(clients.Academic, clients.Announcement, clients.User)
api.RegisterHandlers(router, h)

addr := ":8080"
Expand Down
85 changes: 85 additions & 0 deletions internal/handler/course_registration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package handler

import (
"net/http"

"github.com/gin-gonic/gin"

api "github.com/fun-dotto/admin-bff-api/generated"
"github.com/fun-dotto/admin-bff-api/generated/external/academic_api"
"github.com/fun-dotto/admin-bff-api/internal/middleware"
)

// CourseRegistrationsV1List 履修情報を取得する
func (h *Handler) CourseRegistrationsV1List(c *gin.Context, params api.CourseRegistrationsV1ListParams) {
if !middleware.RequireAnyClaim(c, "admin", "developer") {
return
}

response, err := h.academicClient.CourseRegistrationsV1ListWithResponse(c.Request.Context(), &academic_api.CourseRegistrationsV1ListParams{
UserId: params.UserId,
Year: params.Year,
Semesters: convertSlice[api.DottoFoundationV1CourseSemester, academic_api.DottoFoundationV1CourseSemester](params.Semesters),
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

if response.JSON200 == nil {
c.JSON(response.StatusCode(), gin.H{"error": "unexpected response from upstream"})
return
}

c.JSON(http.StatusOK, gin.H{
"registrations": response.JSON200.CourseRegistrations,
})
}

// CourseRegistrationsV1Create 履修情報を作成する
func (h *Handler) CourseRegistrationsV1Create(c *gin.Context) {
if !middleware.RequireAnyClaim(c, "admin", "developer") {
return
}

var req academic_api.CourseRegistrationRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

response, err := h.academicClient.CourseRegistrationsV1CreateWithResponse(c.Request.Context(), req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

if response.JSON201 == nil {
c.JSON(response.StatusCode(), gin.H{"error": "unexpected response from upstream"})
return
}

c.JSON(http.StatusCreated, gin.H{
"registration": response.JSON201.CourseRegistration,
})
}

// CourseRegistrationsV1Delete 履修情報を削除する
func (h *Handler) CourseRegistrationsV1Delete(c *gin.Context, id string) {
if !middleware.RequireAnyClaim(c, "admin", "developer") {
return
}

response, err := h.academicClient.CourseRegistrationsV1DeleteWithResponse(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

if response.StatusCode() != http.StatusNoContent {
c.JSON(response.StatusCode(), gin.H{"error": "unexpected response from upstream"})
return
}

c.Status(http.StatusNoContent)
}
56 changes: 56 additions & 0 deletions internal/handler/course_registration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package handler

import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/fun-dotto/admin-bff-api/generated/external/academic_api"
"github.com/gin-gonic/gin"
)

func TestCourseRegistrationsV1Create_ProxiesAcademicAPI(t *testing.T) {
gin.SetMode(gin.TestMode)

var gotMethod string
var gotPath string
var gotBody academic_api.CourseRegistrationRequest
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotMethod = r.Method
gotPath = r.URL.Path

if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
t.Fatalf("decode request body: %v", err)
}
Comment on lines +20 to +26
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

The httptest server handler calls t.Fatalf from its own goroutine (net/http serves requests concurrently). In Go tests, Fatal/FailNow should be called only from the test goroutine; this can lead to confusing behavior and makes the test harder to reason about. Capture assertion failures via a channel (or store them and assert after the handler call) instead of calling t.Fatalf inside the server handler.

Copilot uses AI. Check for mistakes.

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_, _ = w.Write([]byte(`{"courseRegistration":{"id":"reg-1","userId":"user-1","subject":{"id":"subject-1","name":"Algorithms","faculties":[]}}}`))
}))
defer server.Close()

h := newTestHandler(t, server.URL)
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(
http.MethodPost,
"/v1/courseRegistrations",
bytes.NewBufferString(`{"subjectId":"subject-1","userId":"user-1"}`),
)
c.Request.Header.Set("Content-Type", "application/json")
setAdminClaim(c)

h.CourseRegistrationsV1Create(c)

if rec.Code != http.StatusCreated {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusCreated)
}
if gotMethod != http.MethodPost || gotPath != "/v1/courseRegistrations" {
t.Fatalf("method/path = %s %s", gotMethod, gotPath)
}
if gotBody.SubjectId != "subject-1" || gotBody.UserId != "user-1" {
t.Fatalf("unexpected upstream request body: %+v", gotBody)
}
}
7 changes: 7 additions & 0 deletions internal/handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,33 @@ import (
api "github.com/fun-dotto/admin-bff-api/generated"
"github.com/fun-dotto/admin-bff-api/generated/external/academic_api"
"github.com/fun-dotto/admin-bff-api/generated/external/announcement_api"
"github.com/fun-dotto/admin-bff-api/generated/external/user_api"
)

type Handler struct {
academicClient *academic_api.ClientWithResponses
announcementClient *announcement_api.ClientWithResponses
userClient *user_api.ClientWithResponses
}

func NewHandler(
academicClient *academic_api.ClientWithResponses,
announcementClient *announcement_api.ClientWithResponses,
userClient *user_api.ClientWithResponses,
) *Handler {
if academicClient == nil {
panic("academicClient is required")
}
if announcementClient == nil {
panic("announcementClient is required")
}
if userClient == nil {
panic("userClient is required")
}
return &Handler{
academicClient: academicClient,
announcementClient: announcementClient,
userClient: userClient,
}
}

Expand Down
58 changes: 58 additions & 0 deletions internal/handler/personal_calendar_item_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package handler

import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"

api "github.com/fun-dotto/admin-bff-api/generated"
"github.com/gin-gonic/gin"
)

func TestPersonalCalendarItemsV1List_ProxiesAcademicAPI(t *testing.T) {
gin.SetMode(gin.TestMode)

wantDate := time.Date(2026, 3, 26, 9, 0, 0, 0, time.UTC)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
if got := query.Get("userId"); got != "user-1" {
t.Fatalf("userId = %q, want %q", got, "user-1")
}
if got := query.Get("dates"); got == "" {
t.Fatal("dates query parameter is empty")
}
Comment on lines +18 to +25
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

The httptest server handler calls t.Fatalf/t.Fatal from its own goroutine. In Go tests, Fatal/FailNow should be called only from the test goroutine; consider reporting failures back to the main test goroutine via a channel and asserting after the request completes.

Copilot uses AI. Check for mistakes.

w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"personalCalendarItems":[{"date":"2026-03-26T09:00:00Z","slot":{"dayOfWeek":"Thursday","period":"first"},"timetableItem":{"id":"item-1","subject":{"id":"subject-1","name":"Algorithms"},"slot":{"dayOfWeek":"Thursday","period":"first"},"rooms":[]}}]}`))
}))
defer server.Close()

h := newTestHandler(t, server.URL)
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodGet, "/v1/personalCalendarItems", nil)
setAdminClaim(c)

h.PersonalCalendarItemsV1List(c, api.PersonalCalendarItemsV1ListParams{
UserId: "user-1",
Dates: []time.Time{wantDate},
})

if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
}

var body struct {
PersonalCalendarItems []struct {
Date string `json:"date"`
} `json:"personalCalendarItems"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
t.Fatalf("unmarshal response: %v", err)
}
if len(body.PersonalCalendarItems) != 1 || body.PersonalCalendarItems[0].Date != "2026-03-26T09:00:00Z" {
t.Fatalf("unexpected response body: %s", rec.Body.String())
}
}
148 changes: 148 additions & 0 deletions internal/handler/room.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package handler

import (
"net/http"

"github.com/gin-gonic/gin"

api "github.com/fun-dotto/admin-bff-api/generated"
"github.com/fun-dotto/admin-bff-api/generated/external/academic_api"
"github.com/fun-dotto/admin-bff-api/internal/middleware"
)

// RoomsV1List 教室一覧を取得する
func (h *Handler) RoomsV1List(c *gin.Context, params api.RoomsV1ListParams) {
if !middleware.RequireAnyClaim(c, "admin", "developer") {
return
}

response, err := h.academicClient.RoomsV1ListWithResponse(c.Request.Context(), &academic_api.RoomsV1ListParams{
Floor: convertSlicePtr[api.DottoFoundationV1Floor, academic_api.DottoFoundationV1Floor](params.Floor),
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

if response.JSON200 == nil {
c.JSON(response.StatusCode(), gin.H{"error": "unexpected response from upstream"})
return
}

c.JSON(http.StatusOK, response.JSON200)
}
Comment on lines +13 to +33
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

These handlers implement previously-stubbed room endpoints, but there are no corresponding tests to verify request/response proxying (query/body pass-through, status codes, auth claim enforcement). Adding at least a minimal httptest-based proxy test for one read and one write path would help prevent regressions.

Copilot uses AI. Check for mistakes.

// RoomsV1Create 教室を作成する
func (h *Handler) RoomsV1Create(c *gin.Context) {
if !middleware.RequireAnyClaim(c, "admin", "developer") {
return
}

var req academic_api.RoomRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

response, err := h.academicClient.RoomsV1CreateWithResponse(c.Request.Context(), req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

if response.JSON201 == nil {
c.JSON(response.StatusCode(), gin.H{"error": "unexpected response from upstream"})
return
}

c.JSON(http.StatusCreated, response.JSON201)
}

// RoomsV1Detail 教室を詳細取得する
func (h *Handler) RoomsV1Detail(c *gin.Context, id string) {
if !middleware.RequireAnyClaim(c, "admin", "developer") {
return
}

response, err := h.academicClient.RoomsV1DetailWithResponse(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

if response.JSON200 == nil {
c.JSON(response.StatusCode(), gin.H{"error": "unexpected response from upstream"})
return
}

c.JSON(http.StatusOK, response.JSON200)
}

// RoomsV1Update 教室を更新する
func (h *Handler) RoomsV1Update(c *gin.Context, id string) {
if !middleware.RequireAnyClaim(c, "admin", "developer") {
return
}

var req academic_api.RoomRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

response, err := h.academicClient.RoomsV1UpdateWithResponse(c.Request.Context(), id, req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

if response.JSON200 == nil {
c.JSON(response.StatusCode(), gin.H{"error": "unexpected response from upstream"})
return
}

c.JSON(http.StatusOK, response.JSON200)
}

// RoomsV1Delete 教室を削除する
func (h *Handler) RoomsV1Delete(c *gin.Context, id string) {
if !middleware.RequireAnyClaim(c, "admin", "developer") {
return
}

response, err := h.academicClient.RoomsV1DeleteWithResponse(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

if response.StatusCode() != http.StatusNoContent {
c.JSON(response.StatusCode(), gin.H{"error": "unexpected response from upstream"})
return
}

c.Status(http.StatusNoContent)
}

// ReservationsV1List 教室の予約一覧を取得する
func (h *Handler) ReservationsV1List(c *gin.Context, id string, params api.ReservationsV1ListParams) {
if !middleware.RequireAnyClaim(c, "admin", "developer") {
return
}

response, err := h.academicClient.ReservationsV1ListWithResponse(c.Request.Context(), id, &academic_api.ReservationsV1ListParams{
From: params.From,
Until: params.Until,
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

if response.JSON200 == nil {
c.JSON(response.StatusCode(), gin.H{"error": "unexpected response from upstream"})
return
}

c.JSON(http.StatusOK, response.JSON200)
}
Loading
Loading