mirror of
https://github.com/jsiebens/ionscale.git
synced 2026-03-31 15:07:49 +01:00
chore: restructure test setup and add some initial web login flow tests
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
package tsn
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/views"
|
||||
)
|
||||
|
||||
type Condition = func(*ipnstate.Status) bool
|
||||
|
||||
func Connected() Condition {
|
||||
return func(status *ipnstate.Status) bool {
|
||||
return status.CurrentTailnet != nil
|
||||
}
|
||||
}
|
||||
|
||||
func HasTailnet(tailnet string) Condition {
|
||||
return func(status *ipnstate.Status) bool {
|
||||
return status.CurrentTailnet != nil && status.CurrentTailnet.Name == tailnet
|
||||
}
|
||||
}
|
||||
|
||||
func HasTag(tag string) Condition {
|
||||
return func(status *ipnstate.Status) bool {
|
||||
return status.Self != nil && status.Self.Tags != nil && views.SliceContains[string](*status.Self.Tags, tag)
|
||||
}
|
||||
}
|
||||
|
||||
func NeedsMachineAuth() Condition {
|
||||
return func(status *ipnstate.Status) bool {
|
||||
return status.BackendState == "NeedsMachineAuth"
|
||||
}
|
||||
}
|
||||
|
||||
func IsRunning() Condition {
|
||||
return func(status *ipnstate.Status) bool {
|
||||
return status.BackendState == "Running"
|
||||
}
|
||||
}
|
||||
|
||||
func HasUser(email string) Condition {
|
||||
return func(status *ipnstate.Status) bool {
|
||||
if status.Self == nil {
|
||||
return false
|
||||
}
|
||||
userID := status.Self.UserID
|
||||
if u, ok := status.User[userID]; ok {
|
||||
return u.LoginName == email
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func PeerCount(expected int) Condition {
|
||||
return func(status *ipnstate.Status) bool {
|
||||
return len(status.Peers()) == expected
|
||||
}
|
||||
}
|
||||
|
||||
func HasCapability(capability tailcfg.NodeCapability) Condition {
|
||||
return func(status *ipnstate.Status) bool {
|
||||
self := status.Self
|
||||
|
||||
if self == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if slices.Contains(self.Capabilities, capability) {
|
||||
return true
|
||||
}
|
||||
|
||||
if _, ok := self.CapMap[capability]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func IsMissingCapability(capability tailcfg.NodeCapability) Condition {
|
||||
return func(status *ipnstate.Status) bool {
|
||||
self := status.Self
|
||||
|
||||
if slices.Contains(self.Capabilities, capability) {
|
||||
return false
|
||||
}
|
||||
|
||||
if _, ok := self.CapMap[capability]; ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
package tsn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/ory/dockertest/v3"
|
||||
"github.com/stretchr/testify/require"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func New(t *testing.T, name, loginServer string, resource *dockertest.Resource, retry func(func() error) error) *TailscaleNode {
|
||||
return &TailscaleNode{
|
||||
t: t,
|
||||
loginServer: loginServer,
|
||||
hostname: name,
|
||||
resource: resource,
|
||||
retry: retry,
|
||||
}
|
||||
}
|
||||
|
||||
type TailscaleNode struct {
|
||||
t *testing.T
|
||||
loginServer string
|
||||
hostname string
|
||||
resource *dockertest.Resource
|
||||
retry func(func() error) error
|
||||
}
|
||||
|
||||
func (t *TailscaleNode) Hostname() string {
|
||||
return t.hostname
|
||||
}
|
||||
|
||||
func (t *TailscaleNode) Up(authkey string) error {
|
||||
t.mustExecTailscaleCmd("up", "--login-server", t.loginServer, "--authkey", authkey)
|
||||
return t.WaitFor(Connected())
|
||||
}
|
||||
|
||||
func (t *TailscaleNode) LoginWithOidc(flags ...UpFlag) (int, error) {
|
||||
check := func(stdout, stderr string) bool {
|
||||
return strings.Contains(stderr, "To authenticate, visit:")
|
||||
}
|
||||
|
||||
cmd := []string{"up", "--login-server", t.loginServer}
|
||||
for _, f := range flags {
|
||||
cmd = append(cmd, f...)
|
||||
}
|
||||
|
||||
_, stderr, err := t.execTailscaleCmdWithCheck(check, cmd...)
|
||||
require.NoError(t.t, err)
|
||||
|
||||
urlStr := strings.ReplaceAll(stderr, "To authenticate, visit:\n\n\t", "")
|
||||
urlStr = strings.TrimSpace(urlStr)
|
||||
|
||||
u, err := url.Parse(urlStr)
|
||||
require.NoError(t.t, err)
|
||||
|
||||
q := u.Query()
|
||||
q.Set("oidc", "true")
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
responseCode, err := t.curl(u)
|
||||
require.NoError(t.t, err)
|
||||
|
||||
if responseCode == http.StatusOK {
|
||||
return responseCode, t.WaitFor(Connected())
|
||||
}
|
||||
|
||||
return responseCode, nil
|
||||
}
|
||||
|
||||
func (t *TailscaleNode) IPv4() string {
|
||||
return t.mustExecTailscaleCmd("ip", "-4")
|
||||
}
|
||||
|
||||
func (t *TailscaleNode) IPv6() string {
|
||||
return t.mustExecTailscaleCmd("ip", "-6")
|
||||
}
|
||||
|
||||
func (t *TailscaleNode) WaitFor(c Condition, additional ...Condition) error {
|
||||
err := t.retry(func() error {
|
||||
out, _, err := t.execTailscaleCmd("status", "--json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var status ipnstate.Status
|
||||
if err := json.Unmarshal([]byte(out), &status); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !c(&status) {
|
||||
return fmt.Errorf("condition not met")
|
||||
}
|
||||
|
||||
for _, a := range additional {
|
||||
if !a(&status) {
|
||||
return fmt.Errorf("condition not met")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *TailscaleNode) Check(c Condition, additional ...Condition) error {
|
||||
out, _, err := t.execTailscaleCmd("status", "--json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var status ipnstate.Status
|
||||
if err := json.Unmarshal([]byte(out), &status); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !c(&status) {
|
||||
return fmt.Errorf("condition not met")
|
||||
}
|
||||
|
||||
for _, a := range additional {
|
||||
if !a(&status) {
|
||||
return fmt.Errorf("condition not met")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TailscaleNode) Ping(target string) error {
|
||||
result, _, err := t.execTailscaleCmd("ping", "--timeout=1s", "--c=10", "--until-direct=true", target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.Contains(result, "pong") && !strings.Contains(result, "is local") {
|
||||
return fmt.Errorf("ping failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TailscaleNode) curl(url *url.URL) (int, error) {
|
||||
cmd := []string{"curl", "-L", "-s", "-o", "/dev/null", "-w", "%{http_code}", url.String()}
|
||||
stdout, _, err := execCmd(t.resource, cmd...)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return strconv.Atoi(stdout)
|
||||
}
|
||||
|
||||
func (t *TailscaleNode) execTailscaleCmd(cmd ...string) (string, string, error) {
|
||||
i := append([]string{"/app/tailscale", "--socket=/tmp/tailscaled.sock"}, cmd...)
|
||||
return execCmd(t.resource, i...)
|
||||
}
|
||||
|
||||
func (t *TailscaleNode) execTailscaleCmdWithCheck(check func(string, string) bool, cmd ...string) (string, string, error) {
|
||||
i := append([]string{"/app/tailscale", "--socket=/tmp/tailscaled.sock"}, cmd...)
|
||||
return execCmdWithCheck(t.resource, check, i...)
|
||||
}
|
||||
|
||||
func (t *TailscaleNode) mustExecTailscaleCmd(cmd ...string) string {
|
||||
i := append([]string{"/app/tailscale", "--socket=/tmp/tailscaled.sock"}, cmd...)
|
||||
s, _, err := execCmd(t.resource, i...)
|
||||
require.NoError(t.t, err)
|
||||
return s
|
||||
}
|
||||
|
||||
func execCmd(resource *dockertest.Resource, cmd ...string) (string, string, error) {
|
||||
return execCmdWithCheck(resource, nil, cmd...)
|
||||
}
|
||||
|
||||
func execCmdWithCheck(resource *dockertest.Resource, check func(string, string) bool, cmd ...string) (string, string, error) {
|
||||
tr := strings.TrimSpace
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
|
||||
type result struct {
|
||||
exitCode int
|
||||
err error
|
||||
}
|
||||
|
||||
resultChan := make(chan result, 1)
|
||||
checkChan := make(chan bool, 1)
|
||||
|
||||
go func() {
|
||||
exitCode, err := resource.Exec(cmd, dockertest.ExecOptions{StdOut: &stdout, StdErr: &stderr})
|
||||
resultChan <- result{exitCode, err}
|
||||
}()
|
||||
|
||||
if check != nil {
|
||||
done := make(chan bool)
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
defer func() {
|
||||
ticker.Stop()
|
||||
done <- true
|
||||
close(done)
|
||||
}()
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-ticker.C:
|
||||
if check(tr(stdout.String()), tr(stderr.String())) {
|
||||
checkChan <- true
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
select {
|
||||
case <-checkChan:
|
||||
return tr(stdout.String()), tr(stderr.String()), nil
|
||||
case res := <-resultChan:
|
||||
if res.err != nil {
|
||||
return stdout.String(), stderr.String(), res.err
|
||||
}
|
||||
|
||||
if res.exitCode != 0 {
|
||||
return tr(stdout.String()), tr(stderr.String()), fmt.Errorf("invalid exit code %d", res.exitCode)
|
||||
}
|
||||
|
||||
return tr(stdout.String()), tr(stderr.String()), nil
|
||||
case <-time.After(60 * time.Second):
|
||||
return tr(stdout.String()), tr(stderr.String()), fmt.Errorf("command timed out")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package tsn
|
||||
|
||||
type UpFlag = []string
|
||||
|
||||
func WithAdvertiseTags(tags string) UpFlag {
|
||||
return []string{"--advertise-tags", tags}
|
||||
}
|
||||
Reference in New Issue
Block a user