package web import ( "errors" "net/http" "net/http/httptest" "slices" "testing" ) func TestInstallerOperationResourcesCoverSharedMutableState(t *testing.T) { resources := installerOperationResources("mpv-flatpak", "app-1", "/opt/mpv", "/tmp/mpv-config") for _, required := range []string{ resourceInstallerMutation, resourceManagerConfig, resourceMPVConfig, resourceKeyring, "method:mpv-flatpak", "app:app-1", "package-manager:flatpak", } { if !slices.Contains(resources, required) { t.Errorf("installer resources missing %q: %v", required, resources) } } } func TestUnlikeInstallerMethodsConflictOnSharedResources(t *testing.T) { manager := NewJobManager() first, ok := manager.CreateJobWithResources( "install", "mpv-flatpak", "MPV", installerOperationResources("mpv-flatpak", "app-1", "/tmp/a", "/tmp/config-a")..., ) if !ok { t.Fatal("first job failed to acquire resources") } if second, ok := manager.CreateJobWithResources( "install", "iina-app", "IINA", installerOperationResources("iina-app", "app-2", "/tmp/b", "/tmp/config-b")..., ); ok || second != nil { t.Fatal("unlike installer methods overlapped on shared mutable state") } manager.SetError(first.ID, errors.New("test stopped"), "test cleanup") } func TestWithOperationLeaseRejectsConflictAndReleasesAfterRequest(t *testing.T) { server := &Server{jobManager: NewJobManager()} called := 0 handler := server.withOperationLease(func(w http.ResponseWriter, _ *http.Request) { called++ w.WriteHeader(http.StatusNoContent) }, resourceMPVConfig) release, ok := server.jobManager.TryAcquireResources("active-job", resourceMPVConfig) if !ok { t.Fatal("failed to establish conflicting lease") } request := httptest.NewRequest(http.MethodPost, "/api/config/apply", nil) response := httptest.NewRecorder() handler(response, request) if response.Code != http.StatusConflict || called != 0 { t.Fatalf("conflict response = %d, called = %d", response.Code, called) } release() response = httptest.NewRecorder() handler(response, request) if response.Code != http.StatusNoContent || called != 1 { t.Fatalf("unleased response = %d, called = %d", response.Code, called) } if releaseAgain, ok := server.jobManager.TryAcquireResources("next-request", resourceMPVConfig); !ok { t.Fatal("request handler did not release its lease") } else { releaseAgain() } } func TestWithOperationLeaseDoesNotBlockReads(t *testing.T) { server := &Server{jobManager: NewJobManager()} handler := server.withOperationLease(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNoContent) }, resourceMPVConfig) release, ok := server.jobManager.TryAcquireResources("active-job", resourceMPVConfig) if !ok { t.Fatal("failed to establish lease") } defer release() response := httptest.NewRecorder() handler(response, httptest.NewRequest(http.MethodGet, "/api/config/settings", nil)) if response.Code != http.StatusNoContent { t.Fatalf("read-only request was blocked: %d", response.Code) } } func TestAllReconciliationRoutesRespectInstallerLease(t *testing.T) { server := &Server{jobManager: NewJobManager(), authToken: "test-token", shutdownCh: make(chan struct{})} _, _, err := server.Start("127.0.0.1:0") if err != nil { t.Fatal(err) } defer server.Shutdown() job, ok := server.jobManager.CreateJobWithResources("install", "fixture", "MPV", resourceManagerConfig) if !ok { t.Fatal("could not establish installer lease") } defer server.jobManager.CompleteJob(job.ID) for _, path := range []string{"/api/apps/check-updates", "/api/apps/refresh-installed", "/api/manifest/refresh"} { t.Run(path, func(t *testing.T) { request := httptest.NewRequest(http.MethodPost, "http://127.0.0.1"+path, nil) request.AddCookie(&http.Cookie{Name: authTokenCookieName, Value: "test-token"}) response := httptest.NewRecorder() server.httpServer.Handler.ServeHTTP(response, request) if response.Code != http.StatusConflict { t.Fatalf("reconciliation bypassed the installer lease: %d %s", response.Code, response.Body.String()) } }) } }