// Copyright (C) 2025 wangyusong // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package rpc import ( "bytes" "context" "encoding/json" "errors" "io" "net/http" "net/http/httptest" "testing" . "github.com/onsi/gomega" "github.com/glidea/zenfeed/pkg/test" ) func TestAPI(t *testing.T) { RegisterTestingT(t) type TestRequest struct { Name string `json:"name"` } type TestResponse struct { Greeting string `json:"greeting"` } type givenDetail struct { handler Handler[TestRequest, TestResponse] } type whenDetail struct { method string requestBody string } type thenExpected struct { statusCode int responseBody string } successHandler := func(ctx context.Context, req *TestRequest) (*TestResponse, error) { return &TestResponse{Greeting: "Hello, " + req.Name}, nil } badRequestHandler := func(ctx context.Context, req *TestRequest) (*TestResponse, error) { return nil, ErrBadRequest(errors.New("invalid request")) } notFoundHandler := func(ctx context.Context, req *TestRequest) (*TestResponse, error) { return nil, ErrNotFound(errors.New("resource not found")) } internalErrorHandler := func(ctx context.Context, req *TestRequest) (*TestResponse, error) { return nil, ErrInternal(errors.New("server error")) } genericErrorHandler := func(ctx context.Context, req *TestRequest) (*TestResponse, error) { return nil, errors.New("generic error") } tests := []test.Case[givenDetail, whenDetail, thenExpected]{ { Scenario: "Successful request", Given: "a handler that returns a successful response", When: "making a valid request", Then: "should return 200 OK with the expected response", GivenDetail: givenDetail{ handler: successHandler, }, WhenDetail: whenDetail{ method: http.MethodPost, requestBody: `{"name":"World"}`, }, ThenExpected: thenExpected{ statusCode: http.StatusOK, responseBody: `{"greeting":"Hello, World"}`, }, }, { Scenario: "Empty request body", Given: "a handler that returns a successful response", When: "making a request with empty body", Then: "should return 200 OK with default values", GivenDetail: givenDetail{ handler: successHandler, }, WhenDetail: whenDetail{ method: http.MethodPost, requestBody: "", }, ThenExpected: thenExpected{ statusCode: http.StatusOK, responseBody: `{"greeting":"Hello, "}`, }, }, { Scenario: "Invalid JSON request", Given: "a handler that processes JSON", When: "making a request with invalid JSON", Then: "should return 400 Bad Request", GivenDetail: givenDetail{ handler: successHandler, }, WhenDetail: whenDetail{ method: http.MethodPost, requestBody: `{"name":`, }, ThenExpected: thenExpected{ statusCode: http.StatusBadRequest, }, }, { Scenario: "Bad request error", Given: "a handler that returns a bad request error", When: "making a request that triggers a bad request error", Then: "should return 400 Bad Request with error details", GivenDetail: givenDetail{ handler: badRequestHandler, }, WhenDetail: whenDetail{ method: http.MethodPost, requestBody: `{"name":"World"}`, }, ThenExpected: thenExpected{ statusCode: http.StatusBadRequest, responseBody: `{"code":400,"message":"invalid request"}`, }, }, { Scenario: "Not found error", Given: "a handler that returns a not found error", When: "making a request that triggers a not found error", Then: "should return 404 Not Found with error details", GivenDetail: givenDetail{ handler: notFoundHandler, }, WhenDetail: whenDetail{ method: http.MethodPost, requestBody: `{"name":"World"}`, }, ThenExpected: thenExpected{ statusCode: http.StatusNotFound, responseBody: `{"code":404,"message":"resource not found"}`, }, }, { Scenario: "Internal server error", Given: "a handler that returns an internal server error", When: "making a request that triggers an internal server error", Then: "should return 500 Internal Server Error with error details", GivenDetail: givenDetail{ handler: internalErrorHandler, }, WhenDetail: whenDetail{ method: http.MethodPost, requestBody: `{"name":"World"}`, }, ThenExpected: thenExpected{ statusCode: http.StatusInternalServerError, responseBody: `{"code":500,"message":"server error"}`, }, }, { Scenario: "Generic error", Given: "a handler that returns a generic error", When: "making a request that triggers a generic error", Then: "should return 500 Internal Server Error", GivenDetail: givenDetail{ handler: genericErrorHandler, }, WhenDetail: whenDetail{ method: http.MethodPost, requestBody: `{"name":"World"}`, }, ThenExpected: thenExpected{ statusCode: http.StatusInternalServerError, }, }, } for _, tt := range tests { t.Run(tt.Scenario, func(t *testing.T) { // Given. handler := API(tt.GivenDetail.handler) // When. var req *http.Request if tt.WhenDetail.requestBody == "" { req = httptest.NewRequest(tt.WhenDetail.method, "/test", nil) } else { req = httptest.NewRequest(tt.WhenDetail.method, "/test", bytes.NewBufferString(tt.WhenDetail.requestBody)) } rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) // Then. Expect(rec.Code).To(Equal(tt.ThenExpected.statusCode)) if tt.ThenExpected.responseBody != "" { var expected, actual interface{} err := json.Unmarshal([]byte(tt.ThenExpected.responseBody), &expected) Expect(err).NotTo(HaveOccurred()) body, err := io.ReadAll(rec.Body) Expect(err).NotTo(HaveOccurred()) err = json.Unmarshal(body, &actual) Expect(err).NotTo(HaveOccurred()) Expect(actual).To(Equal(expected)) } }) } }