//go:build unit package admin import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "testing" "time" "github.com/Wei-Shaw/sub2api/internal/service" "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" ) type systemHandlerUpdateServiceStub struct { performErr error updateInfo *service.UpdateInfo checkErr error checkForces []bool performCall int } func (s *systemHandlerUpdateServiceStub) CheckUpdate(_ context.Context, force bool) (*service.UpdateInfo, error) { s.checkForces = append(s.checkForces, force) return s.updateInfo, s.checkErr } func (s *systemHandlerUpdateServiceStub) PerformUpdate(context.Context) error { s.performCall++ return s.performErr } func (s *systemHandlerUpdateServiceStub) Rollback() error { return nil } type systemUpdateResponseEnvelope struct { Code int `json:"code"` Message string `json:"message"` Data struct { Message string `json:"message"` AlreadyUpToDate bool `json:"already_up_to_date"` CurrentVersion string `json:"current_version"` LatestVersion string `json:"latest_version"` OperationID string `json:"operation_id"` } `json:"data"` } type systemUpdateErrorEnvelope struct { Code int `json:"code"` Message string `json:"message"` } func newSystemHandlerTestRouter(t *testing.T, updateSvc *systemHandlerUpdateServiceStub, repo *memoryIdempotencyRepoStub) *gin.Engine { t.Helper() gin.SetMode(gin.TestMode) service.SetDefaultIdempotencyCoordinator(nil) t.Cleanup(func() { service.SetDefaultIdempotencyCoordinator(nil) }) lockSvc := service.NewSystemOperationLockService(repo, service.IdempotencyConfig{ ProcessingTimeout: time.Second, SystemOperationTTL: time.Minute, }) handler := NewSystemHandler(updateSvc, lockSvc) router := gin.New() router.POST("/api/v1/admin/system/update", handler.PerformUpdate) return router } func requireSystemLockStatus(t *testing.T, repo *memoryIdempotencyRepoStub, wantStatus string) { t.Helper() repo.mu.Lock() defer repo.mu.Unlock() for _, record := range repo.data { if record.Status == wantStatus { return } } t.Fatalf("system lock status %q not found in records: %#v", wantStatus, repo.data) } func TestSystemHandlerPerformUpdateAlreadyUpToDateReturnsOK(t *testing.T) { updateSvc := &systemHandlerUpdateServiceStub{ performErr: service.ErrNoUpdateAvailable, updateInfo: &service.UpdateInfo{ CurrentVersion: "0.1.132", LatestVersion: "0.1.132", HasUpdate: false, }, } repo := newMemoryIdempotencyRepoStub() router := newSystemHandlerTestRouter(t, updateSvc, repo) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/system/update", nil) req.Header.Set("Idempotency-Key", "already-up-to-date") router.ServeHTTP(rec, req) require.Equal(t, http.StatusOK, rec.Code) require.Equal(t, 1, updateSvc.performCall) require.Equal(t, []bool{false}, updateSvc.checkForces) requireSystemLockStatus(t, repo, service.IdempotencyStatusSucceeded) var body systemUpdateResponseEnvelope require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &body)) require.Equal(t, 0, body.Code) require.Equal(t, "success", body.Message) require.Equal(t, "Already up to date", body.Data.Message) require.True(t, body.Data.AlreadyUpToDate) require.Equal(t, "0.1.132", body.Data.CurrentVersion) require.Equal(t, "0.1.132", body.Data.LatestVersion) require.NotEmpty(t, body.Data.OperationID) } func TestSystemHandlerPerformUpdateFailureStillReturnsInternalError(t *testing.T) { updateSvc := &systemHandlerUpdateServiceStub{ performErr: errors.New("download failed"), } repo := newMemoryIdempotencyRepoStub() router := newSystemHandlerTestRouter(t, updateSvc, repo) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/system/update", nil) req.Header.Set("Idempotency-Key", "real-failure") router.ServeHTTP(rec, req) require.Equal(t, http.StatusInternalServerError, rec.Code) require.Equal(t, 1, updateSvc.performCall) require.Empty(t, updateSvc.checkForces) requireSystemLockStatus(t, repo, service.IdempotencyStatusFailedRetryable) var body systemUpdateErrorEnvelope require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &body)) require.Equal(t, http.StatusInternalServerError, body.Code) require.Equal(t, "internal error", body.Message) }