Некоторые проверки не удались
CI / test (push) Successful in 2m5s
Docker / Build and publish worker image (push) Failing after 31s
190 строки
6.3 KiB
Go
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
|
|
)
|