feat(github): support PR URLs in task creation dialog (#379)
This commit is contained in:
@@ -33,6 +33,7 @@ func (c *Controller) RegisterHTTPRoutes(router *gin.Engine) {
|
||||
api.GET("/task-prs/:taskId", c.httpGetTaskPR)
|
||||
|
||||
api.GET("/prs/:owner/:repo/:number", c.httpGetPRFeedback)
|
||||
api.GET("/prs/:owner/:repo/:number/info", c.httpGetPRInfo)
|
||||
api.POST("/prs/:owner/:repo/:number/reviews", c.httpSubmitReview)
|
||||
|
||||
api.GET("/watches/pr", c.httpListPRWatches)
|
||||
@@ -107,6 +108,35 @@ func (c *Controller) httpGetPRFeedback(ctx *gin.Context) {
|
||||
ctx.JSON(http.StatusOK, feedback)
|
||||
}
|
||||
|
||||
func (c *Controller) httpGetPRInfo(ctx *gin.Context) {
|
||||
owner := ctx.Param("owner")
|
||||
repo := ctx.Param("repo")
|
||||
numberStr := ctx.Param("number")
|
||||
number, err := strconv.Atoi(numberStr)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid PR number"})
|
||||
return
|
||||
}
|
||||
pr, err := c.service.GetPR(ctx.Request.Context(), owner, repo, number)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
var apiErr *GitHubAPIError
|
||||
if errors.As(err, &apiErr) {
|
||||
switch apiErr.StatusCode {
|
||||
case http.StatusNotFound:
|
||||
status = http.StatusNotFound
|
||||
case http.StatusUnauthorized:
|
||||
status = http.StatusUnauthorized
|
||||
case http.StatusForbidden:
|
||||
status = http.StatusForbidden
|
||||
}
|
||||
}
|
||||
ctx.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, pr)
|
||||
}
|
||||
|
||||
func (c *Controller) httpSubmitReview(ctx *gin.Context) {
|
||||
owner := ctx.Param("owner")
|
||||
repo := ctx.Param("repo")
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/kandev/kandev/internal/common/logger"
|
||||
)
|
||||
|
||||
// stubClient implements Client with no-op defaults; override fields as needed.
|
||||
type stubClient struct {
|
||||
getPRFunc func(ctx context.Context, owner, repo string, number int) (*PR, error)
|
||||
}
|
||||
|
||||
func (s *stubClient) IsAuthenticated(context.Context) (bool, error) { return true, nil }
|
||||
func (s *stubClient) GetAuthenticatedUser(context.Context) (string, error) {
|
||||
return "test-user", nil
|
||||
}
|
||||
func (s *stubClient) GetPR(ctx context.Context, owner, repo string, number int) (*PR, error) {
|
||||
if s.getPRFunc != nil {
|
||||
return s.getPRFunc(ctx, owner, repo, number)
|
||||
}
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
func (s *stubClient) FindPRByBranch(context.Context, string, string, string) (*PR, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubClient) ListAuthoredPRs(context.Context, string, string) ([]*PR, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubClient) ListReviewRequestedPRs(context.Context, string, string, string) ([]*PR, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubClient) ListUserOrgs(context.Context) ([]GitHubOrg, error) { return nil, nil }
|
||||
func (s *stubClient) SearchOrgRepos(context.Context, string, string, int) ([]GitHubRepo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubClient) ListPRReviews(context.Context, string, string, int) ([]PRReview, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubClient) ListPRComments(context.Context, string, string, int, *time.Time) ([]PRComment, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubClient) ListCheckRuns(context.Context, string, string, string) ([]CheckRun, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubClient) GetPRFeedback(context.Context, string, string, int) (*PRFeedback, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubClient) ListPRFiles(context.Context, string, string, int) ([]PRFile, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubClient) ListPRCommits(context.Context, string, string, int) ([]PRCommitInfo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubClient) SubmitReview(context.Context, string, string, int, string, string) error {
|
||||
return nil
|
||||
}
|
||||
func (s *stubClient) ListRepoBranches(context.Context, string, string) ([]RepoBranch, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func newControllerTestLogger() *logger.Logger {
|
||||
log, _ := logger.NewLogger(logger.LoggingConfig{
|
||||
Level: "error",
|
||||
Format: "json",
|
||||
})
|
||||
return log
|
||||
}
|
||||
|
||||
func setupControllerTest(client Client) (*gin.Engine, *Controller) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
log := newControllerTestLogger()
|
||||
svc := NewService(client, "pat", nil, nil, nil, log)
|
||||
ctrl := NewController(svc, log)
|
||||
router := gin.New()
|
||||
ctrl.RegisterHTTPRoutes(router)
|
||||
return router, ctrl
|
||||
}
|
||||
|
||||
func TestHttpGetPRInfo_Success(t *testing.T) {
|
||||
sc := &stubClient{
|
||||
getPRFunc: func(_ context.Context, owner, repo string, number int) (*PR, error) {
|
||||
if owner != "acme" || repo != "widget" || number != 42 {
|
||||
t.Errorf("unexpected params: %s/%s#%d", owner, repo, number)
|
||||
}
|
||||
return &PR{Number: 42, Title: "feat: add widget"}, nil
|
||||
},
|
||||
}
|
||||
router, _ := setupControllerTest(sc)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/github/prs/acme/widget/42/info", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var pr PR
|
||||
if err := json.NewDecoder(w.Body).Decode(&pr); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
if pr.Number != 42 {
|
||||
t.Errorf("expected PR number 42, got %d", pr.Number)
|
||||
}
|
||||
if pr.Title != "feat: add widget" {
|
||||
t.Errorf("expected title 'feat: add widget', got %q", pr.Title)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHttpGetPRInfo_InvalidNumber(t *testing.T) {
|
||||
router, _ := setupControllerTest(&stubClient{})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/github/prs/acme/widget/abc/info", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHttpGetPRInfo_ServiceError(t *testing.T) {
|
||||
sc := &stubClient{
|
||||
getPRFunc: func(context.Context, string, string, int) (*PR, error) {
|
||||
return nil, fmt.Errorf("not found")
|
||||
},
|
||||
}
|
||||
router, _ := setupControllerTest(sc)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/github/prs/acme/widget/99/info", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Fatalf("expected 500, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
@@ -399,7 +399,15 @@ func (s *Service) SyncTaskPR(ctx context.Context, taskID string, feedback *PRFee
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- PR feedback (live) ---
|
||||
// --- PR info and feedback (live) ---
|
||||
|
||||
// GetPR fetches basic PR details from GitHub.
|
||||
func (s *Service) GetPR(ctx context.Context, owner, repo string, number int) (*PR, error) {
|
||||
if s.client == nil {
|
||||
return nil, fmt.Errorf("github client not available")
|
||||
}
|
||||
return s.client.GetPR(ctx, owner, repo, number)
|
||||
}
|
||||
|
||||
// GetPRFeedback fetches live PR feedback from GitHub.
|
||||
func (s *Service) GetPRFeedback(ctx context.Context, owner, repo string, number int) (*PRFeedback, error) {
|
||||
|
||||
@@ -1,10 +1,34 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestGetPR_NilClient(t *testing.T) {
|
||||
svc := &Service{client: nil}
|
||||
_, err := svc.GetPR(context.Background(), "owner", "repo", 1)
|
||||
if err == nil {
|
||||
t.Fatal("expected error when client is nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "github client not available") {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPRFeedback_NilClient(t *testing.T) {
|
||||
svc := &Service{client: nil}
|
||||
_, err := svc.GetPRFeedback(context.Background(), "owner", "repo", 1)
|
||||
if err == nil {
|
||||
t.Fatal("expected error when client is nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "github client not available") {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePRURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
Reference in New Issue
Block a user