Files
worker/internal/webapp/test_helpers.go
Gleb Tv 2c884c5612
Некоторые проверки не удались
CI / test (push) Successful in 2m5s
Docker / Build and publish worker image (push) Failing after 31s
refactor: adopt worker module path
2026-07-13 17:56:12 +03:00

190 строки
6.3 KiB
Go

package webapp
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/cookiejar"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"rocketgit.ru/rsmon/worker/internal/distworker"
)
// _ = assert is a guard so test_helpers.go stays import-clean when
// only the helpers below are needed.
var _ = assert.New
// distworkerHTTPConfig is a local alias for distworker.HTTPConfig so
// the test stub matches the WorkerView interface signature.
type _ = distworker.HTTPConfig
// stubRunner is a minimal WorkerView used by tests. RecentResults
// and RecentNotifications return the slices we configured; the rest
// are simple getters.
type stubRunner struct {
id string
region string
version string
caps []string
lastAck time.Time
token string
rotated time.Time
results []ResultRow
notifs []NotificationRow
// masterUp / masterAt let tests pin a specific
// /api/peer/status response. The default is the zero value
// ("no probe yet" / nil up), matching the runner's pre-Start
// behavior.
masterUp *bool
masterAt time.Time
}
// distworkerHTTPConfig is a local alias for distworker.HTTPConfig so
// the test stub does not pull in the worker package's full surface.
type distworkerHTTPConfig = distworker.HTTPConfig
func (s *stubRunner) HTTPConfig() distworker.HTTPConfig { return distworker.HTTPConfig{} }
func (s *stubRunner) Token() string { return s.token }
func (s *stubRunner) TokenRotatedAt() time.Time { return s.rotated }
func (s *stubRunner) WorkerID() string { return s.id }
func (s *stubRunner) RegionCode() string { return s.region }
func (s *stubRunner) WorkerVersion() string { return s.version }
func (s *stubRunner) WorkerCapabilities() []string { return s.caps }
func (s *stubRunner) LastHeartbeatAck() time.Time { return s.lastAck }
func (s *stubRunner) RecentResults(_ int) []ResultRow { return append([]ResultRow{}, s.results...) }
func (s *stubRunner) RecentNotifications(_ int) []NotificationRow {
return append([]NotificationRow{}, s.notifs...)
}
func (s *stubRunner) MasterStatus() (*bool, time.Time) { return s.masterUp, s.masterAt }
// newTestServer builds an in-memory Server with a stubbed worker
// view and no DB persistence (dataDir is a t.TempDir()). The
// inventory and metrics goroutines are NOT started so tests stay
// hermetic.
func newTestServer(t *testing.T, runner WorkerView) *Server {
t.Helper()
return newTestServerWithBasicAuth(t, runner, "", "")
}
// newTestServerWithBasicAuth is the basic-auth-aware variant of
// newTestServer. login == "" or password == "" disables basic auth.
func newTestServerWithBasicAuth(t *testing.T, runner WorkerView, login, password string) *Server {
t.Helper()
dir := t.TempDir()
cfg := Config{
Addr: "127.0.0.1:0",
DataDir: dir,
BasicAuthLogin: login,
BasicAuthPassword: password,
}
srv, err := New(cfg, &Deps{
Runner: runner,
Version: "test",
BuildDate: "test-build",
StartedAt: time.Now().UTC(),
})
require.NoError(t, err)
t.Cleanup(func() {
_ = srv.Close(context.Background())
})
return srv
}
// newHTTPTestServer wraps srv.Handler in an httptest.Server so tests
// can dial a real loopback port and let cookie jar + redirects work
// naturally.
func newHTTPTestServer(t *testing.T, srv *Server) *httptest.Server {
t.Helper()
ts := httptest.NewServer(srv.Handler())
t.Cleanup(ts.Close)
return ts
}
// httpClient returns a cookie-aware *http.Client that does not
// follow redirects automatically (tests want to see the 302).
func httpClient() *http.Client {
jar, _ := cookiejar.New(nil)
return &http.Client{Jar: jar, CheckRedirect: func(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse
}}
}
// loginAsFirstRun provisions a fresh password via the same path the
// cmd binary uses (first-run hash written to the store, plaintext
// returned), then logs the operator in by POSTing the credentials.
// The cookie jar is returned so callers can re-use the session.
//
// The first-run flow is frictionless: the login lands on /overview
// without bouncing through /web/change-password. The flag is still
// recorded on the user row for a future hardening knob.
func loginAsFirstRun(t *testing.T, baseURL string, srv *Server) (*http.Client, string) {
t.Helper()
plain, err := GenerateFirstRunPassword()
require.NoError(t, err)
hash, err := HashPassword(plain)
require.NoError(t, err)
_, err = srv.store.CreateFirstRunUser(context.Background(), hash)
require.NoError(t, err)
c := httpClient()
form := url.Values{}
form.Set("password", plain)
req, _ := http.NewRequest(http.MethodPost, baseURL+"/web/login", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := c.Do(req)
require.NoError(t, err)
defer resp.Body.Close() //nolint:errcheck
require.Equal(t, http.StatusFound, resp.StatusCode,
"login should redirect on success")
require.Equal(t, "/overview", resp.Header.Get("Location"))
return c, plain
}
// provisionFirstRunUser seeds the store with a user whose bcrypt hash
// matches the supplied plaintext and whose requires_change flag is
// set. loginAsFirstRun wraps the round trip that consumes the seed.
func provisionFirstRunUser(t *testing.T, srv *Server, plain string) {
t.Helper()
hash, err := HashPassword(plain)
require.NoError(t, err)
_, err = srv.store.CreateFirstRunUser(context.Background(), hash)
require.NoError(t, err)
}
// clearRequiresChange flips the requires_change flag off after the
// operator has changed their password (in tests we skip the change
// form to assert the regular page flow).
func clearRequiresChange(t *testing.T, srv *Server) {
t.Helper()
user, err := srv.store.GetUser(context.Background())
require.NoError(t, err)
require.NoError(t, srv.store.MarkRequiresChange(context.Background(), user.ID))
// MarkRequiresChange sets it to 1; we want it off. Use a direct
// helper.
_, err = srv.store.db.ExecContext(context.Background(),
`UPDATE webapp_users SET requires_change = 0 WHERE id = ?`, user.ID)
require.NoError(t, err)
}
var (
_ = os.Getenv
_ = filepath.Join
_ = json.Marshal
_ = bytes.NewReader
_ = io.Copy
_ = fmt.Sprintf
)