feat: publish standalone worker
Separate worker packaging and service lifecycle from the control plane.
Этот коммит содержится в:
189
internal/webapp/test_helpers.go
Обычный файл
189
internal/webapp/test_helpers.go
Обычный файл
@@ -0,0 +1,189 @@
|
||||
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"
|
||||
|
||||
"rsgit.ru/rsmon/rsmon/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
|
||||
)
|
||||
Ссылка в новой задаче
Block a user