mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-02 11:39:06 +02:00
Merge branch 'main' into embedded-vnc
This commit is contained in:
@@ -16,6 +16,10 @@ type Client interface {
|
||||
Job(ctx context.Context, msgHandler func(msg *proto.JobRequest) *proto.JobResponse) error
|
||||
Register(setupKey string, jwtToken string, sysInfo *system.Info, sshKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error)
|
||||
Login(sysInfo *system.Info, sshKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error)
|
||||
// ExtendAuthSession refreshes the peer's SSO session deadline using a fresh JWT.
|
||||
// Returns the new absolute deadline; zero time when the server reports the peer
|
||||
// is not eligible for session extension.
|
||||
ExtendAuthSession(sysInfo *system.Info, jwtToken string) (*proto.ExtendAuthSessionResponse, error)
|
||||
GetDeviceAuthorizationFlow() (*proto.DeviceAuthorizationFlow, error)
|
||||
GetPKCEAuthorizationFlow() (*proto.PKCEAuthorizationFlow, error)
|
||||
GetNetworkMap(sysInfo *system.Info) (*proto.NetworkMap, error)
|
||||
|
||||
@@ -17,8 +17,8 @@ import (
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/netbirdio/management-integrations/integrations"
|
||||
ephemeral_manager "github.com/netbirdio/netbird/management/internals/modules/peers/ephemeral/manager"
|
||||
"github.com/netbirdio/netbird/management/server/integrations/integrated_validator/validator"
|
||||
|
||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller"
|
||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map/update_channel"
|
||||
@@ -89,7 +89,7 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) {
|
||||
gomock.Any(),
|
||||
gomock.Any(),
|
||||
).
|
||||
Return(true, nil).
|
||||
Return(true, context.Background(), nil).
|
||||
AnyTimes()
|
||||
|
||||
peersManger := peers.NewManager(store, permissionsManagerMock)
|
||||
@@ -103,7 +103,7 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ia, _ := integrations.NewIntegratedValidator(ctx, peersManger, settingsManagerMock, eventStore, cacheStore)
|
||||
ia, _ := validator.NewIntegratedValidator(ctx, peersManger, settingsManagerMock, eventStore, cacheStore)
|
||||
|
||||
metrics, err := telemetry.NewDefaultAppMetrics(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -607,6 +607,61 @@ func (c *GrpcClient) Login(sysInfo *system.Info, pubSSHKey []byte, dnsLabels dom
|
||||
return c.login(&proto.LoginRequest{Meta: infoToMetaData(sysInfo), PeerKeys: keys, DnsLabels: dnsLabels.ToPunycodeList()})
|
||||
}
|
||||
|
||||
// ExtendAuthSession refreshes the peer's SSO session deadline on the management
|
||||
// server using a freshly issued JWT. The tunnel is untouched: no network map
|
||||
// sync, no peer reconnect. Returns the new absolute UTC deadline (zero time
|
||||
// when the server reports the field empty).
|
||||
func (c *GrpcClient) ExtendAuthSession(sysInfo *system.Info, jwtToken string) (*proto.ExtendAuthSessionResponse, error) {
|
||||
if !c.ready() {
|
||||
return nil, errors.New(errMsgNoMgmtConnection)
|
||||
}
|
||||
|
||||
serverKey, err := c.getServerPublicKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reqBody, err := encryption.EncryptMessage(*serverKey, c.key, &proto.ExtendAuthSessionRequest{
|
||||
JwtToken: jwtToken,
|
||||
Meta: infoToMetaData(sysInfo),
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("failed to encrypt extend auth session message: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp *proto.EncryptedMessage
|
||||
operation := func() error {
|
||||
mgmCtx, cancel := context.WithTimeout(context.Background(), ConnectTimeout)
|
||||
defer cancel()
|
||||
|
||||
var err error
|
||||
resp, err = c.realClient.ExtendAuthSession(mgmCtx, &proto.EncryptedMessage{
|
||||
WgPubKey: c.key.PublicKey().String(),
|
||||
Body: reqBody,
|
||||
})
|
||||
if err != nil {
|
||||
if s, ok := gstatus.FromError(err); ok && s.Code() == codes.Canceled {
|
||||
return err
|
||||
}
|
||||
return backoff.Permanent(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := backoff.Retry(operation, nbgrpc.Backoff(c.ctx)); err != nil {
|
||||
log.Errorf("failed to extend auth session on Management Service: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := &proto.ExtendAuthSessionResponse{}
|
||||
if err := encryption.DecryptMessage(*serverKey, c.key, resp.Body, out); err != nil {
|
||||
log.Errorf("failed to decrypt extend auth session response: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// GetDeviceAuthorizationFlow returns a device authorization flow information.
|
||||
// It also takes care of encrypting and decrypting messages.
|
||||
func (c *GrpcClient) GetDeviceAuthorizationFlow() (*proto.DeviceAuthorizationFlow, error) {
|
||||
|
||||
@@ -14,6 +14,7 @@ type MockClient struct {
|
||||
SyncFunc func(ctx context.Context, sysInfo *system.Info, msgHandler func(msg *proto.SyncResponse) error) error
|
||||
RegisterFunc func(setupKey string, jwtToken string, info *system.Info, sshKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error)
|
||||
LoginFunc func(info *system.Info, sshKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error)
|
||||
ExtendAuthSessionFunc func(info *system.Info, jwtToken string) (*proto.ExtendAuthSessionResponse, error)
|
||||
GetDeviceAuthorizationFlowFunc func() (*proto.DeviceAuthorizationFlow, error)
|
||||
GetPKCEAuthorizationFlowFunc func() (*proto.PKCEAuthorizationFlow, error)
|
||||
GetServerURLFunc func() string
|
||||
@@ -65,6 +66,13 @@ func (m *MockClient) Login(info *system.Info, sshKey []byte, dnsLabels domain.Li
|
||||
return m.LoginFunc(info, sshKey, dnsLabels)
|
||||
}
|
||||
|
||||
func (m *MockClient) ExtendAuthSession(info *system.Info, jwtToken string) (*proto.ExtendAuthSessionResponse, error) {
|
||||
if m.ExtendAuthSessionFunc == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return m.ExtendAuthSessionFunc(info, jwtToken)
|
||||
}
|
||||
|
||||
func (m *MockClient) GetDeviceAuthorizationFlow() (*proto.DeviceAuthorizationFlow, error) {
|
||||
if m.GetDeviceAuthorizationFlowFunc == nil {
|
||||
return nil, nil
|
||||
|
||||
@@ -143,6 +143,10 @@ type Client struct {
|
||||
|
||||
// ReverseProxyDomains NetBird reverse proxy domains APIs
|
||||
ReverseProxyDomains *ReverseProxyDomainsAPI
|
||||
|
||||
// ReverseProxyTokens account-scoped proxy access tokens used to register
|
||||
// self-hosted (bring-your-own-proxy) `netbird proxy` instances.
|
||||
ReverseProxyTokens *ReverseProxyTokensAPI
|
||||
}
|
||||
|
||||
// New initialize new Client instance using PAT token
|
||||
@@ -204,6 +208,7 @@ func (c *Client) initialize() {
|
||||
c.ReverseProxyServices = &ReverseProxyServicesAPI{c}
|
||||
c.ReverseProxyClusters = &ReverseProxyClustersAPI{c}
|
||||
c.ReverseProxyDomains = &ReverseProxyDomainsAPI{c}
|
||||
c.ReverseProxyTokens = &ReverseProxyTokensAPI{c}
|
||||
}
|
||||
|
||||
// NewRequest creates and executes new management API request
|
||||
|
||||
@@ -2,6 +2,8 @@ package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/url"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||
)
|
||||
@@ -11,7 +13,10 @@ type ReverseProxyClustersAPI struct {
|
||||
c *Client
|
||||
}
|
||||
|
||||
// List lists all available proxy clusters
|
||||
// List lists all available proxy clusters. Each cluster is enriched with the
|
||||
// capability flags reported by its connected proxies (supports_custom_ports,
|
||||
// supports_crowdsec, private, etc.), so callers can render UX gates without
|
||||
// a follow-up round-trip.
|
||||
func (a *ReverseProxyClustersAPI) List(ctx context.Context) ([]api.ProxyCluster, error) {
|
||||
resp, err := a.c.NewRequest(ctx, "GET", "/api/reverse-proxies/clusters", nil, nil)
|
||||
if err != nil {
|
||||
@@ -23,3 +28,24 @@ func (a *ReverseProxyClustersAPI) List(ctx context.Context) ([]api.ProxyCluster,
|
||||
ret, err := parseResponse[[]api.ProxyCluster](resp)
|
||||
return ret, err
|
||||
}
|
||||
|
||||
// Delete removes every self-hosted (BYOP) proxy registration for the given
|
||||
// cluster address owned by the calling account. Shared clusters operated by
|
||||
// NetBird cannot be deleted via this endpoint; the server returns 404 / 400
|
||||
// for cluster addresses the account does not own.
|
||||
func (a *ReverseProxyClustersAPI) Delete(ctx context.Context, clusterAddress string) error {
|
||||
// Guard against the empty input: url.PathEscape("") returns "" which
|
||||
// would collapse the request URL onto the collection endpoint and
|
||||
// silently delete nothing (or 405 depending on routing).
|
||||
if clusterAddress == "" {
|
||||
return errors.New("clusterAddress is required")
|
||||
}
|
||||
resp, err := a.c.NewRequest(ctx, "DELETE", "/api/reverse-proxies/clusters/"+url.PathEscape(clusterAddress), nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
//go:build integration
|
||||
|
||||
package rest_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/client/rest"
|
||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||
"github.com/netbirdio/netbird/shared/management/http/util"
|
||||
)
|
||||
|
||||
func boolPtr(b bool) *bool { return &b }
|
||||
|
||||
var testCluster = api.ProxyCluster{
|
||||
Id: "cluster-1",
|
||||
Address: "proxy.netbird.local",
|
||||
Type: "shared",
|
||||
Online: true,
|
||||
ConnectedProxies: 2,
|
||||
SupportsCustomPorts: boolPtr(true),
|
||||
RequireSubdomain: boolPtr(false),
|
||||
SupportsCrowdsec: boolPtr(false),
|
||||
Private: boolPtr(true),
|
||||
}
|
||||
|
||||
func TestReverseProxyClusters_List_200(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/reverse-proxies/clusters", func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "GET", r.Method, "List must use GET")
|
||||
retBytes, _ := json.Marshal([]api.ProxyCluster{testCluster})
|
||||
_, err := w.Write(retBytes)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
ret, err := c.ReverseProxyClusters.List(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ret, 1)
|
||||
assert.Equal(t, testCluster.Id, ret[0].Id)
|
||||
assert.Equal(t, testCluster.Address, ret[0].Address)
|
||||
require.NotNil(t, ret[0].Private, "private capability must round-trip through the client")
|
||||
assert.True(t, *ret[0].Private, "private capability must reflect the server value")
|
||||
})
|
||||
}
|
||||
|
||||
func TestReverseProxyClusters_List_Err(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/reverse-proxies/clusters", func(w http.ResponseWriter, r *http.Request) {
|
||||
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 500})
|
||||
w.WriteHeader(500)
|
||||
_, err := w.Write(retBytes)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
ret, err := c.ReverseProxyClusters.List(context.Background())
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, ret)
|
||||
})
|
||||
}
|
||||
|
||||
func TestReverseProxyClusters_Delete_200(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
// PathEscape on "proxy.netbird.local" leaves it intact; the route mux
|
||||
// matches the unescaped form. Sanity-check both the method and that
|
||||
// path-escaping doesn't double-encode the dotted address.
|
||||
mux.HandleFunc("/api/reverse-proxies/clusters/proxy.netbird.local", func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "DELETE", r.Method, "Delete must use DELETE")
|
||||
w.WriteHeader(200)
|
||||
})
|
||||
err := c.ReverseProxyClusters.Delete(context.Background(), "proxy.netbird.local")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestReverseProxyClusters_Delete_Err(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/reverse-proxies/clusters/proxy.netbird.local", func(w http.ResponseWriter, r *http.Request) {
|
||||
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
|
||||
w.WriteHeader(404)
|
||||
_, err := w.Write(retBytes)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
err := c.ReverseProxyClusters.Delete(context.Background(), "proxy.netbird.local")
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestReverseProxyClusters_Delete_EmptyAddress guards against an empty
|
||||
// clusterAddress reaching the wire — that would collapse the URL onto
|
||||
// the collection endpoint instead of a specific cluster. The client
|
||||
// must short-circuit with a typed error before any request is issued.
|
||||
func TestReverseProxyClusters_Delete_EmptyAddress(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/reverse-proxies/clusters/", func(http.ResponseWriter, *http.Request) {
|
||||
t.Fatal("empty clusterAddress must be rejected client-side; no request should reach the server")
|
||||
})
|
||||
err := c.ReverseProxyClusters.Delete(context.Background(), "")
|
||||
assert.Error(t, err, "empty clusterAddress must surface as an error")
|
||||
})
|
||||
}
|
||||
@@ -116,8 +116,8 @@ func TestReverseProxyServices_Create_200(t *testing.T) {
|
||||
Name: "test-service",
|
||||
Domain: "test.example.com",
|
||||
Enabled: true,
|
||||
Auth: api.ServiceAuthConfig{},
|
||||
Targets: []api.ServiceTarget{testServiceTarget},
|
||||
Auth: &api.ServiceAuthConfig{},
|
||||
Targets: &[]api.ServiceTarget{testServiceTarget},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testService.Id, ret.Id)
|
||||
@@ -136,8 +136,8 @@ func TestReverseProxyServices_Create_Err(t *testing.T) {
|
||||
Name: "test-service",
|
||||
Domain: "test.example.com",
|
||||
Enabled: true,
|
||||
Auth: api.ServiceAuthConfig{},
|
||||
Targets: []api.ServiceTarget{testServiceTarget},
|
||||
Auth: &api.ServiceAuthConfig{},
|
||||
Targets: &[]api.ServiceTarget{testServiceTarget},
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "No", err.Error())
|
||||
@@ -154,8 +154,9 @@ func TestReverseProxyServices_Create_WithPerTargetOptions(t *testing.T) {
|
||||
var req api.ServiceRequest
|
||||
require.NoError(t, json.Unmarshal(reqBytes, &req))
|
||||
|
||||
require.Len(t, req.Targets, 1)
|
||||
target := req.Targets[0]
|
||||
require.NotNil(t, req.Targets, "targets must be set on the request")
|
||||
require.Len(t, *req.Targets, 1)
|
||||
target := (*req.Targets)[0]
|
||||
require.NotNil(t, target.Options, "options should be present")
|
||||
opts := target.Options
|
||||
require.NotNil(t, opts.SkipTlsVerify, "skip_tls_verify should be present")
|
||||
@@ -177,8 +178,8 @@ func TestReverseProxyServices_Create_WithPerTargetOptions(t *testing.T) {
|
||||
Name: "test-service",
|
||||
Domain: "test.example.com",
|
||||
Enabled: true,
|
||||
Auth: api.ServiceAuthConfig{},
|
||||
Targets: []api.ServiceTarget{
|
||||
Auth: &api.ServiceAuthConfig{},
|
||||
Targets: &[]api.ServiceTarget{
|
||||
{
|
||||
TargetId: "peer-123",
|
||||
TargetType: "peer",
|
||||
@@ -216,8 +217,8 @@ func TestReverseProxyServices_Update_200(t *testing.T) {
|
||||
Name: "updated-service",
|
||||
Domain: "test.example.com",
|
||||
Enabled: true,
|
||||
Auth: api.ServiceAuthConfig{},
|
||||
Targets: []api.ServiceTarget{testServiceTarget},
|
||||
Auth: &api.ServiceAuthConfig{},
|
||||
Targets: &[]api.ServiceTarget{testServiceTarget},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testService.Id, ret.Id)
|
||||
@@ -236,8 +237,8 @@ func TestReverseProxyServices_Update_Err(t *testing.T) {
|
||||
Name: "updated-service",
|
||||
Domain: "test.example.com",
|
||||
Enabled: true,
|
||||
Auth: api.ServiceAuthConfig{},
|
||||
Targets: []api.ServiceTarget{testServiceTarget},
|
||||
Auth: &api.ServiceAuthConfig{},
|
||||
Targets: &[]api.ServiceTarget{testServiceTarget},
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "No", err.Error())
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/url"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||
)
|
||||
|
||||
// ReverseProxyTokensAPI exposes the account-scoped proxy access tokens that
|
||||
// self-hosted (bring-your-own-proxy) deployments use to register a
|
||||
// `netbird proxy` instance with management. Tokens are bound to the
|
||||
// calling account; revoking a token disconnects every proxy that
|
||||
// authenticated with it.
|
||||
type ReverseProxyTokensAPI struct {
|
||||
c *Client
|
||||
}
|
||||
|
||||
// List returns every proxy token the calling account has minted, including
|
||||
// already-revoked entries. The plain token is never returned — only the
|
||||
// metadata (id, name, created_at, last_used, revoked).
|
||||
func (a *ReverseProxyTokensAPI) List(ctx context.Context) ([]api.ProxyToken, error) {
|
||||
resp, err := a.c.NewRequest(ctx, "GET", "/api/reverse-proxies/proxy-tokens", nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
ret, err := parseResponse[[]api.ProxyToken](resp)
|
||||
return ret, err
|
||||
}
|
||||
|
||||
// Create mints a fresh account-scoped proxy token. The returned
|
||||
// ProxyTokenCreated.PlainToken is shown only once — callers must persist
|
||||
// it immediately. Subsequent reads will only expose the token metadata,
|
||||
// not the secret material.
|
||||
func (a *ReverseProxyTokensAPI) Create(ctx context.Context, request api.ProxyTokenRequest) (*api.ProxyTokenCreated, error) {
|
||||
requestBytes, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := a.c.NewRequest(ctx, "POST", "/api/reverse-proxies/proxy-tokens", bytes.NewReader(requestBytes), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
ret, err := parseResponse[api.ProxyTokenCreated](resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
// Delete revokes a previously-issued proxy token by ID. Revoked tokens
|
||||
// remain in List output (with revoked=true) so operators can audit which
|
||||
// credentials existed; the plain secret can no longer authenticate any
|
||||
// new proxy registration.
|
||||
func (a *ReverseProxyTokensAPI) Delete(ctx context.Context, tokenID string) error {
|
||||
// Guard against the empty input: url.PathEscape("") returns "" which
|
||||
// would collapse the request URL onto the collection endpoint and
|
||||
// silently delete nothing (or 405 depending on routing).
|
||||
if tokenID == "" {
|
||||
return errors.New("tokenID is required")
|
||||
}
|
||||
resp, err := a.c.NewRequest(ctx, "DELETE", "/api/reverse-proxies/proxy-tokens/"+url.PathEscape(tokenID), nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
//go:build integration
|
||||
|
||||
package rest_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/client/rest"
|
||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||
"github.com/netbirdio/netbird/shared/management/http/util"
|
||||
)
|
||||
|
||||
func intPtr(v int) *int { return &v }
|
||||
|
||||
var testProxyToken = api.ProxyToken{
|
||||
Id: "tok-1",
|
||||
Name: "ci-runner",
|
||||
CreatedAt: time.Date(2026, 5, 21, 9, 0, 0, 0, time.UTC),
|
||||
Revoked: false,
|
||||
}
|
||||
|
||||
var testProxyTokenCreated = api.ProxyTokenCreated{
|
||||
Id: "tok-1",
|
||||
Name: "ci-runner",
|
||||
CreatedAt: time.Date(2026, 5, 21, 9, 0, 0, 0, time.UTC),
|
||||
PlainToken: "nbproxy_abcdef0123456789",
|
||||
Revoked: false,
|
||||
}
|
||||
|
||||
func TestReverseProxyTokens_List_200(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/reverse-proxies/proxy-tokens", func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "GET", r.Method, "List must use GET")
|
||||
retBytes, _ := json.Marshal([]api.ProxyToken{testProxyToken})
|
||||
_, err := w.Write(retBytes)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
ret, err := c.ReverseProxyTokens.List(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ret, 1)
|
||||
assert.Equal(t, testProxyToken.Id, ret[0].Id)
|
||||
assert.Equal(t, testProxyToken.Name, ret[0].Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestReverseProxyTokens_List_Err(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/reverse-proxies/proxy-tokens", func(w http.ResponseWriter, r *http.Request) {
|
||||
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 500})
|
||||
w.WriteHeader(500)
|
||||
_, err := w.Write(retBytes)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
ret, err := c.ReverseProxyTokens.List(context.Background())
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, ret)
|
||||
})
|
||||
}
|
||||
|
||||
func TestReverseProxyTokens_Create_200(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/reverse-proxies/proxy-tokens", func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "POST", r.Method, "Create must use POST")
|
||||
body, err := io.ReadAll(r.Body)
|
||||
require.NoError(t, err)
|
||||
var req api.ProxyTokenRequest
|
||||
require.NoError(t, json.Unmarshal(body, &req), "server must receive a valid ProxyTokenRequest body")
|
||||
assert.Equal(t, "ci-runner", req.Name, "name must round-trip through the client")
|
||||
require.NotNil(t, req.ExpiresIn, "expires_in must be sent when provided")
|
||||
assert.Equal(t, 3600, *req.ExpiresIn, "expires_in value must round-trip unchanged")
|
||||
|
||||
retBytes, _ := json.Marshal(testProxyTokenCreated)
|
||||
_, err = w.Write(retBytes)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
ret, err := c.ReverseProxyTokens.Create(context.Background(), api.ProxyTokenRequest{
|
||||
Name: "ci-runner",
|
||||
ExpiresIn: intPtr(3600),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testProxyTokenCreated.Id, ret.Id)
|
||||
assert.Equal(t, testProxyTokenCreated.PlainToken, ret.PlainToken,
|
||||
"PlainToken must be returned to the caller — it's the one-shot secret")
|
||||
})
|
||||
}
|
||||
|
||||
func TestReverseProxyTokens_Create_Err(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/reverse-proxies/proxy-tokens", func(w http.ResponseWriter, r *http.Request) {
|
||||
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Bad", Code: 400})
|
||||
w.WriteHeader(400)
|
||||
_, err := w.Write(retBytes)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
ret, err := c.ReverseProxyTokens.Create(context.Background(), api.ProxyTokenRequest{Name: ""})
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, ret)
|
||||
})
|
||||
}
|
||||
|
||||
func TestReverseProxyTokens_Delete_200(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/reverse-proxies/proxy-tokens/tok-1", func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "DELETE", r.Method, "Delete must use DELETE")
|
||||
w.WriteHeader(200)
|
||||
})
|
||||
err := c.ReverseProxyTokens.Delete(context.Background(), "tok-1")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestReverseProxyTokens_Delete_Err(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/reverse-proxies/proxy-tokens/tok-1", func(w http.ResponseWriter, r *http.Request) {
|
||||
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
|
||||
w.WriteHeader(404)
|
||||
_, err := w.Write(retBytes)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
err := c.ReverseProxyTokens.Delete(context.Background(), "tok-1")
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestReverseProxyTokens_Delete_EmptyID guards against an empty tokenID
|
||||
// reaching the wire — url.PathEscape("") would collapse the URL onto
|
||||
// the collection endpoint.
|
||||
func TestReverseProxyTokens_Delete_EmptyID(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/reverse-proxies/proxy-tokens/", func(http.ResponseWriter, *http.Request) {
|
||||
t.Fatal("empty tokenID must be rejected client-side; no request should reach the server")
|
||||
})
|
||||
err := c.ReverseProxyTokens.Delete(context.Background(), "")
|
||||
assert.Error(t, err, "empty tokenID must surface as an error")
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user