Ctrl K
ComunidadGuías…

action/command/api_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package command

import (
	"context"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	"connectrpc.com/connect"
	"github.com/stretchr/testify/require"

	"github.com/bytebase/bytebase/action/world"
)

func TestNewClientWithOptionsUsesServiceAccountAuth(t *testing.T) {
	t.Parallel()

	httpClient := &http.Client{}
	client, err := newClient("https://example.com", "", "sa@example.com", "secret", clientOptions{
		httpClient: httpClient,
		pageSize:   -1,
	})
	require.NoError(t, err)

	t.Cleanup(client.close)

	require.Same(t, httpClient, client.httpClient)
	require.Equal(t, int32(100), client.options.pageSize)
	require.Equal(t, "sa@example.com", client.serviceAccount)
	require.Equal(t, "secret", client.serviceAccountSecret)
}

func TestCustomHeaderFlag(t *testing.T) {
	t.Parallel()

	headers := http.Header{}
	var headerErr error
	flag := newCustomHeaderFlag(&headers, &headerErr)

	require.NoError(t, flag.Set("Cookie: CF_Authorization=token"))
	require.NoError(t, flag.Set("X-Request-Id: run-123"))
	require.NoError(t, headerErr)
	require.Equal(t, "CF_Authorization=token", headers.Get("Cookie"))
	require.Equal(t, "run-123", headers.Get("X-Request-Id"))
}

func TestCustomHeaderFlagRejectsInvalidHeaders(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name  string
		value string
	}{
		{
			name:  "missing separator",
			value: "Cookie",
		},
		{
			name:  "empty name",
			value: ": value",
		},
		{
			name:  "newline in value",
			value: "Cookie: value\nX-Other: injected",
		},
		{
			name:  "authorization is managed by bytebase action",
			value: "Authorization: Bearer token",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			headers := http.Header{}
			var headerErr error
			require.NoError(t, newCustomHeaderFlag(&headers, &headerErr).Set(tt.value))
			require.Error(t, headerErr)
		})
	}
}

func TestCustomHeaderFlagDoesNotLeakHeaderValueInFormatError(t *testing.T) {
	t.Parallel()

	headers := http.Header{}
	var headerErr error
	secret := "Cookie CF_Authorization=secret-token"
	require.NoError(t, newCustomHeaderFlag(&headers, &headerErr).Set(secret))
	require.Error(t, headerErr)
	require.NotContains(t, headerErr.Error(), "secret-token")
	require.NotContains(t, headerErr.Error(), secret)
}

func TestCustomHeadersAreSentOnLoginRequest(t *testing.T) {
	t.Parallel()

	customHeaders := http.Header{}
	customHeaders.Set("Cookie", "CF_Authorization=token")
	customHeaders.Set("X-Request-Id", "run-123")

	transport := roundTripFunc(func(req *http.Request) (*http.Response, error) {
		require.Equal(t, "CF_Authorization=token", req.Header.Get("Cookie"))
		require.Equal(t, "run-123", req.Header.Get("X-Request-Id"))
		return &http.Response{
			StatusCode: http.StatusUnauthorized,
			Header:     make(http.Header),
			Body:       io.NopCloser(strings.NewReader("unauthorized")),
			Request:    req,
		}, nil
	})

	httpClient := &http.Client{Transport: transport}
	client, err := newClient("https://example.com", "", "sa@example.com", "secret", clientOptions{
		httpClient:    httpClient,
		customHeaders: customHeaders,
	})
	require.NoError(t, err)
	t.Cleanup(client.close)
	refreshToken := getTokenRefresher(client.httpClient, "sa@example.com", "secret", "https://example.com")

	_, err = refreshToken(t.Context())
	require.Error(t, err)
}

func TestNewClientDoesNotMutateProvidedHTTPClientTransport(t *testing.T) {
	t.Parallel()

	customHeaders := http.Header{}
	customHeaders.Set("Cookie", "CF_Authorization=token")
	transport := &closeIdleRoundTripper{}
	httpClient := &http.Client{Transport: transport}

	client, err := newClient("https://example.com", "token", "", "", clientOptions{
		httpClient:    httpClient,
		customHeaders: customHeaders,
	})
	require.NoError(t, err)
	t.Cleanup(client.close)

	require.Same(t, transport, httpClient.Transport)
	require.NotSame(t, httpClient, client.httpClient)
}

func TestCustomHeaderTransportAddsCustomHeaders(t *testing.T) {
	t.Parallel()

	customHeaders := http.Header{}
	customHeaders.Set("Cookie", "CF_Authorization=token")
	customHeaders.Set("X-Request-Id", "run-123")

	transport := newCustomHeaderTransport(roundTripFunc(func(req *http.Request) (*http.Response, error) {
		require.Equal(t, "CF_Authorization=token", req.Header.Get("Cookie"))
		require.Equal(t, "run-123", req.Header.Get("X-Request-Id"))
		return &http.Response{
			StatusCode: http.StatusOK,
			Header:     make(http.Header),
			Body:       io.NopCloser(strings.NewReader("ok")),
			Request:    req,
		}, nil
	}), customHeaders)

	resp, err := transport.RoundTrip(httptest.NewRequest(http.MethodGet, "https://example.com", nil))
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, resp.StatusCode)
	require.NoError(t, resp.Body.Close())
}

func TestCustomHeaderTransportClosesIdleConnections(t *testing.T) {
	t.Parallel()

	base := &closeIdleRoundTripper{}
	transport := newCustomHeaderTransport(base, http.Header{})
	transport.(interface{ CloseIdleConnections() }).CloseIdleConnections()
	require.True(t, base.closed)
}

type roundTripFunc func(req *http.Request) (*http.Response, error)

func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
	return f(req)
}

type closeIdleRoundTripper struct {
	closed bool
}

func (*closeIdleRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
	return &http.Response{
		StatusCode: http.StatusOK,
		Header:     make(http.Header),
		Body:       io.NopCloser(strings.NewReader("ok")),
		Request:    req,
	}, nil
}

func (t *closeIdleRoundTripper) CloseIdleConnections() {
	t.closed = true
}

func TestNewClientWithAccessTokenUsesAccessTokenAuth(t *testing.T) {
	t.Parallel()

	client, err := newClient("https://example.com", "token", "", "", defaultClientOptions())
	require.NoError(t, err)

	t.Cleanup(client.close)

	require.Equal(t, "", client.serviceAccount)
	require.Equal(t, "", client.serviceAccountSecret)
	require.Equal(t, 120*time.Second, client.httpClient.Timeout)
}

func TestRejectedAccessTokenFailsAfterOneReauthentication(t *testing.T) {
	t.Parallel()

	var requests atomic.Int32
	transport := roundTripFunc(func(req *http.Request) (*http.Response, error) {
		requests.Add(1)
		return &http.Response{
			StatusCode: http.StatusUnauthorized,
			Header:     make(http.Header),
			Body:       io.NopCloser(strings.NewReader("unauthorized")),
			Request:    req,
		}, nil
	})

	opts := defaultClientOptions()
	opts.httpClient = &http.Client{Transport: transport}
	opts.retryConfig = &retryConfig{maxAttempts: 3, initialInterval: time.Millisecond, maxInterval: time.Millisecond}
	client, err := newClient("https://example.com", "null", "", "", opts)
	require.NoError(t, err)
	t.Cleanup(client.close)

	ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
	defer cancel()
	_, err = client.getActuatorInfo(ctx)
	require.Equal(t, connect.CodeUnauthenticated, connect.CodeOf(err))
	require.NoError(t, ctx.Err())
	// The first request carries no token; the refresh loads it, and the retry is rejected.
	require.Equal(t, int32(2), requests.Load())
}

func TestNewClientFromWorldPrefersAccessToken(t *testing.T) {
	t.Parallel()

	w := &world.World{
		URL:                  "https://example.com",
		Timeout:              5 * time.Second,
		AccessToken:          "token",
		ServiceAccount:       "sa@example.com",
		ServiceAccountSecret: "secret",
	}

	client, err := newClientFromWorld(w)
	require.NoError(t, err)

	t.Cleanup(client.close)

	require.Equal(t, 5*time.Second, client.httpClient.Timeout)
	require.Equal(t, "", client.serviceAccount)
	require.Equal(t, "", client.serviceAccountSecret)
}