Files
kandev/apps/backend/internal/linear/mock_controller.go
T
João Salavisa 5936bb6808 test: add e2e coverage and mock backends for jira and linear integrations (#771)
* test: add e2e coverage and mock backends for jira and linear integrations

Adds in-memory MockClient + MockController for both integrations gated by
KANDEV_MOCK_{JIRA,LINEAR}=true, plus apiClient mock helpers and Playwright
specs covering settings and import flows.

Also fixes ValidatedPopover's TriggerButton silently dropping radix Slot's
onClick handler — the popover wouldn't open under any synthetic-click path
(including Playwright). Spreads `...rest` onto the inner Button.

Reworks the integration cleanup hook in test-base as an auto fixture so it
runs for every test using the imported `test` object, not just tests in the
fixture file itself.

* fix: address PR review feedback on jira/linear mocks and e2e specs

- Fix SetAuthResult docstrings: nil simulates an unconfigured state, not the default success.
- Drop unused JSON tags from doneTransitionCall and setStateCall (Go-only test fixtures).
- Track Linear issue insertion order so SearchIssues returns deterministic results instead of map-iteration shuffle.
- Document filterByJQL precedence at the SearchTickets call site so the all-hits-on-no-match fallback is visible.
- Wait for the post-save auth-health probe before forcing a failure in the seeded-failure tests, eliminating a goroutine race.
- Reorder authMethod precedence in setJiraConfig/setLinearConfig to put the spread first so 'caller wins' reads naturally.

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-05-02 14:41:36 +01:00

128 lines
3.8 KiB
Go

package linear
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/kandev/kandev/internal/common/logger"
)
// MockController exposes HTTP endpoints that drive the in-memory MockClient
// during E2E tests. Mounted by RegisterMockRoutes only when the service was
// built with a MockClient.
type MockController struct {
mock *MockClient
store *Store
log *logger.Logger
}
// NewMockController wires the controller to the mock + store so auth-health
// writes can be triggered synchronously from tests.
func NewMockController(mock *MockClient, store *Store, log *logger.Logger) *MockController {
return &MockController{mock: mock, store: store, log: log}
}
// RegisterRoutes mounts the mock control routes under /api/v1/linear/mock.
func (c *MockController) RegisterRoutes(router *gin.Engine) {
api := router.Group("/api/v1/linear/mock")
api.PUT("/auth-result", c.setAuthResult)
api.PUT("/auth-health", c.setAuthHealth)
api.POST("/teams", c.setTeams)
api.POST("/states", c.setStates)
api.POST("/issues", c.addIssues)
api.PUT("/get-issue-error", c.setGetIssueError)
api.DELETE("/reset", c.reset)
}
func (c *MockController) setAuthResult(ctx *gin.Context) {
var req TestConnectionResult
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"})
return
}
r := req
c.mock.SetAuthResult(&r)
ctx.JSON(http.StatusOK, gin.H{"set": true})
}
func (c *MockController) setAuthHealth(ctx *gin.Context) {
var req struct {
WorkspaceID string `json:"workspaceId"`
OK bool `json:"ok"`
Error string `json:"error"`
OrgSlug string `json:"orgSlug"`
}
if err := ctx.ShouldBindJSON(&req); err != nil || req.WorkspaceID == "" {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "workspaceId required"})
return
}
if err := c.store.UpdateAuthHealth(ctx.Request.Context(), req.WorkspaceID, req.OK, req.Error, req.OrgSlug, time.Now().UTC()); err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
ctx.JSON(http.StatusOK, gin.H{"set": true})
}
func (c *MockController) setTeams(ctx *gin.Context) {
var req struct {
Teams []LinearTeam `json:"teams"`
}
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"})
return
}
c.mock.SetTeams(req.Teams)
ctx.JSON(http.StatusOK, gin.H{"count": len(req.Teams)})
}
func (c *MockController) setStates(ctx *gin.Context) {
var req struct {
TeamKey string `json:"teamKey"`
States []LinearWorkflowState `json:"states"`
}
if err := ctx.ShouldBindJSON(&req); err != nil || req.TeamKey == "" {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "teamKey required"})
return
}
c.mock.SetStates(req.TeamKey, req.States)
ctx.JSON(http.StatusOK, gin.H{"count": len(req.States)})
}
func (c *MockController) addIssues(ctx *gin.Context) {
var req struct {
Issues []LinearIssue `json:"issues"`
}
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"})
return
}
for i := range req.Issues {
c.mock.AddIssue(&req.Issues[i])
}
ctx.JSON(http.StatusOK, gin.H{"added": len(req.Issues)})
}
func (c *MockController) setGetIssueError(ctx *gin.Context) {
var req struct {
StatusCode int `json:"statusCode"`
Message string `json:"message"`
}
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"})
return
}
if req.StatusCode == 0 {
c.mock.SetGetIssueError(nil)
} else {
c.mock.SetGetIssueError(&APIError{StatusCode: req.StatusCode, Message: req.Message})
}
ctx.JSON(http.StatusOK, gin.H{"set": true})
}
func (c *MockController) reset(ctx *gin.Context) {
c.mock.Reset()
ctx.JSON(http.StatusOK, gin.H{"reset": true})
}