Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions images/chromium-headful/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ RUN set -eux; \
wget ca-certificates python2 supervisor xclip xdotool \
pulseaudio dbus-x11 xserver-xorg-video-dummy \
libcairo2 libxcb1 libxrandr2 libxv1 libopus0 libvpx7 \
x11-xserver-utils \
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need this for xrandr

gstreamer1.0-plugins-base gstreamer1.0-plugins-good \
gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly \
gstreamer1.0-pulseaudio gstreamer1.0-omx; \
Expand Down
24 changes: 23 additions & 1 deletion images/chromium-headful/run-docker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,29 @@ CHROMIUM_FLAGS="${CHROMIUM_FLAGS:-$CHROMIUM_FLAGS_DEFAULT}"
rm -rf .tmp/chromium
mkdir -p .tmp/chromium
FLAGS_FILE="$(pwd)/.tmp/chromium/flags"
echo "$CHROMIUM_FLAGS" > "$FLAGS_FILE"

# Convert space-separated flags to JSON array format, handling quoted strings
# Use eval to properly parse quoted strings (respects shell quoting)
if [ -n "$CHROMIUM_FLAGS" ]; then
eval "FLAGS_ARRAY=($CHROMIUM_FLAGS)"
else
FLAGS_ARRAY=()
fi

FLAGS_JSON='{"flags":['
FIRST=true
for flag in "${FLAGS_ARRAY[@]}"; do
if [ -n "$flag" ]; then
if [ "$FIRST" = true ]; then
FLAGS_JSON+="\"$flag\""
FIRST=false
else
FLAGS_JSON+=",\"$flag\""
fi
fi
done
FLAGS_JSON+=']}'
echo "$FLAGS_JSON" > "$FLAGS_FILE"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Flag Parsing Vulnerability Causes JSON Malformation

The script's logic for converting CHROMIUM_FLAGS into a JSON array doesn't properly handle flag values containing spaces or JSON special characters. This can result in malformed JSON and introduces a shell injection vulnerability.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Chromium Flag Parsing and JSON Formatting Errors

The new logic for converting Chromium flags to JSON has two issues. First, IFS=' ' read -ra incorrectly splits flags that contain spaces within quotes (e.g., --user-agent="My Custom Agent"). Second, if $CHROMIUM_FLAGS is empty or contains only spaces, the resulting JSON is malformed as {"flags":]} instead of {"flags":[]}.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Shell Code Injection via User-Controlled Flags

The eval "FLAGS_ARRAY=($CHROMIUM_FLAGS)" command in both scripts introduces a command injection vulnerability. Since CHROMIUM_FLAGS can be user-controlled, malicious input could lead to arbitrary shell code execution.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again, not user facing and this is fine

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: JSON Flag Parsing Fails on Special Characters

The JSON generation for Chromium flags doesn't escape special characters within flag values. If a flag contains double quotes, backslashes, or other JSON special characters, the resulting JSON will be invalid. This could cause the downstream flag parsing system to fail or misinterpret flags.

Additional Locations (1)

Fix in Cursor Fix in Web


echo "flags file: $FLAGS_FILE"
cat "$FLAGS_FILE"
Expand Down
27 changes: 26 additions & 1 deletion images/chromium-headful/run-unikernel.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,32 @@ CHROMIUM_FLAGS="${CHROMIUM_FLAGS:-$chromium_flags_default}"
rm -rf .tmp/chromium
mkdir -p .tmp/chromium
FLAGS_DIR=".tmp/chromium"
echo "$CHROMIUM_FLAGS" > "$FLAGS_DIR/flags"

# Convert space-separated flags to JSON array format, handling quoted strings
# Use eval to properly parse quoted strings (respects shell quoting)
if [ -n "$CHROMIUM_FLAGS" ]; then
eval "FLAGS_ARRAY=($CHROMIUM_FLAGS)"
else
FLAGS_ARRAY=()
fi
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Environment Variable Injection Vulnerability

The use of eval "FLAGS_ARRAY=($CHROMIUM_FLAGS)" in both scripts introduces a command injection vulnerability. Since CHROMIUM_FLAGS can be set via environment variables, malicious input could lead to arbitrary shell code execution.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that is fine. It is not for production use case

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Shell Injection via CHROMIUM_FLAGS

The eval command in run-unikernel.sh and run-docker.sh parses CHROMIUM_FLAGS. This creates a security vulnerability, allowing arbitrary command injection if CHROMIUM_FLAGS contains malicious shell code, as it can be controlled by user input or environment variables.

Additional Locations (1)

Fix in Cursor Fix in Web


FLAGS_JSON='{"flags":['
FIRST=true
for flag in "${FLAGS_ARRAY[@]}"; do
if [ -n "$flag" ]; then
if [ "$FIRST" = true ]; then
FLAGS_JSON+="\"$flag\""
FIRST=false
else
FLAGS_JSON+=",\"$flag\""
fi
fi
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Shell Command Injection via eval

The eval command used to parse CHROMIUM_FLAGS in both run-unikernel.sh and run-docker.sh introduces a command injection vulnerability. If CHROMIUM_FLAGS contains malicious shell commands, eval will execute them, potentially leading to arbitrary code execution.

Additional Locations (1)

Fix in Cursor Fix in Web

done
FLAGS_JSON+=']}'
echo "$FLAGS_JSON" > "$FLAGS_DIR/flags"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: JSON Parsing Fails on Quoted Flags

The new JSON flag conversion logic in run-unikernel.sh incorrectly parses CHROMIUM_FLAGS. It fails to properly handle flags containing spaces (like quoted arguments) and does not escape JSON special characters (quotes, backslashes). This results in malformed JSON, preventing Chromium from starting and creating a shell injection vulnerability.

Fix in Cursor Fix in Web


echo "flags file: $FLAGS_DIR/flags"
cat "$FLAGS_DIR/flags"

# Re-create the volume from scratch every run
kraft cloud volume rm "$volume_name" || true
Expand Down
34 changes: 32 additions & 2 deletions images/chromium-headful/xorg.conf
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ Section "Monitor"

# 1920x1080 @ 60.00 Hz (GTF) hsync: 67.08 kHz; pclk: 172.80 MHz
Modeline "1920x1080_60.00" 172.80 1920 2040 2248 2576 1080 1081 1084 1118 -HSync +Vsync
# 1920x1200 @ 60.00 Hz (GTF) hsync: 74.52 kHz; pclk: 193.25 MHz
Modeline "1920x1200_60.00" 193.25 1920 2056 2256 2592 1200 1203 1209 1242 -HSync +Vsync
# 1440x900 @ 60.00 Hz (GTF) hsync: 55.92 kHz; pclk: 106.29 MHz
Modeline "1440x900_60.00" 106.29 1440 1520 1672 1904 900 901 904 932 -HSync +Vsync
# 1280x720 @ 60.00 Hz (GTF) hsync: 44.76 kHz; pclk: 74.48 MHz
Modeline "1280x720_60.00" 74.48 1280 1336 1472 1664 720 721 724 746 -HSync +Vsync
# 1152x648 @ 60.00 Hz (GTF) hsync: 40.26 kHz; pclk: 59.91 MHz
Expand All @@ -62,6 +66,10 @@ Section "Monitor"
Modeline "960x720_60.00" 55.86 960 1008 1104 1248 720 721 724 746 -HSync +Vsync
# 800x600 @ 60.00 Hz (GTF) hsync: 37.32 kHz; pclk: 38.22 MHz
Modeline "800x600_60.00" 38.22 800 832 912 1024 600 601 604 622 -HSync +Vsync
# 2560x1440 @ 60.00 Hz (GTF) hsync: 89.52 kHz; pclk: 312.25 MHz
Modeline "2560x1440_60.00" 312.25 2560 2752 3024 3488 1440 1443 1448 1493 -HSync +Vsync
# 1024x768 @ 60.00 Hz (GTF) hsync: 47.70 kHz; pclk: 63.50 MHz
Modeline "1024x768_60.00" 63.50 1024 1072 1176 1328 768 771 775 798 -HSync +Vsync

# 2560x1440 @ 30.00 Hz (GTF) hsync: 43.95 kHz; pclk: 146.27 MHz
Modeline "2560x1440_30.00" 146.27 2560 2680 2944 3328 1440 1441 1444 1465 -HSync +Vsync
Expand All @@ -79,7 +87,12 @@ Section "Monitor"
Modeline "960x720_30.00" 25.33 960 960 1056 1152 720 721 724 733 -HSync +Vsync
# 800x600 @ 30.00 Hz (GTF) hsync: 18.33 kHz; pclk: 17.01 MHz
Modeline "800x600_30.00" 17.01 800 792 864 928 600 601 604 611 -HSync +Vsync

# 1920x1200 @ 30.00 Hz (GTF) hsync: 36.90 kHz; pclk: 96.00 MHz
Modeline "1920x1200_30.00" 96.00 1920 2000 2200 2528 1200 1203 1209 1235 -HSync +Vsync
# 1440x900 @ 30.00 Hz (GTF) hsync: 27.72 kHz; pclk: 52.80 MHz
Modeline "1440x900_30.00" 52.80 1440 1496 1648 1888 900 901 904 929 -HSync +Vsync
# 1024x768 @ 30.00 Hz (GTF) hsync: 23.55 kHz; pclk: 31.50 MHz
Modeline "1024x768_30.00" 31.50 1024 1048 1152 1280 768 771 775 790 -HSync +Vsync
# 800x1600 @ 30.00 Hz (GTF) hsync: 48.84 kHz; pclk: 51.58 MHz
Modeline "800x1600_30.00" 51.58 800 840 928 1056 1600 1601 1604 1628 -HSync +Vsync

Expand All @@ -93,8 +106,25 @@ Section "Monitor"
Modeline "1600x900_25.00" 45.75 1600 1648 1800 2000 900 903 908 916 -Hsync +Vsync
# 1368x768 @ 24.89 Hz (CVT) hsync: 19.51 kHz; pclk: 33.25 MHz
Modeline "1368x768_25.00" 33.25 1368 1408 1536 1704 768 771 781 784 -Hsync +Vsync
# 1920x1200 @ 25.00 Hz (GTF) hsync: 30.75 kHz; pclk: 80.00 MHz
Modeline "1920x1200_25.00" 80.00 1920 1968 2160 2464 1200 1203 1209 1231 -HSync +Vsync
# 1440x900 @ 25.00 Hz (GTF) hsync: 23.10 kHz; pclk: 44.00 MHz
Modeline "1440x900_25.00" 44.00 1440 1472 1616 1824 900 901 904 925 -HSync +Vsync
# 1024x768 @ 25.00 Hz (GTF) hsync: 19.62 kHz; pclk: 26.25 MHz
Modeline "1024x768_25.00" 26.25 1024 1032 1136 1248 768 771 775 787 -HSync +Vsync
# 800x1600 @ 24.92 Hz (CVT) hsync: 40.53 kHz; pclk: 41.50 MHz
Modeline "800x1600_25.00" 41.50 800 832 912 1024 1600 1603 1613 1626 -Hsync +Vsync

# 2560x1440 @ 10.00 Hz (GTF) hsync: 14.65 kHz; pclk: 48.76 MHz
Modeline "2560x1440_10.00" 48.76 2560 2568 2816 3104 1440 1441 1444 1465 -HSync +Vsync
# 1920x1080 @ 10.00 Hz (GTF) hsync: 10.99 kHz; pclk: 26.73 MHz
Modeline "1920x1080_10.00" 26.73 1920 1920 2104 2304 1080 1081 1084 1099 -HSync +Vsync
# 1920x1200 @ 10.00 Hz (GTF) hsync: 12.30 kHz; pclk: 32.00 MHz
Modeline "1920x1200_10.00" 32.00 1920 1936 2120 2368 1200 1203 1209 1220 -HSync +Vsync
# 1440x900 @ 10.00 Hz (GTF) hsync: 9.24 kHz; pclk: 17.60 MHz
Modeline "1440x900_10.00" 17.60 1440 1424 1560 1680 900 901 904 917 -HSync +Vsync
# 1024x768 @ 10.00 Hz (GTF) hsync: 7.85 kHz; pclk: 10.50 MHz
Modeline "1024x768_10.00" 10.50 1024 1000 1096 1168 768 771 775 778 -HSync +Vsync
EndSection

Section "Screen"
Expand All @@ -105,7 +135,7 @@ Section "Screen"
SubSection "Display"
Viewport 0 0
Depth 24
Modes "1920x1080_60.00" "1280x720_60.00" "1152x648_60.00" "1024x576_60.00" "960x720_60.00" "800x600_60.00" "2560x1440_30.00" "1920x1080_30.00" "1368x768_30.00" "1280x720_30.00" "1152x648_30.00" "1024x576_30.00" "960x720_30.00" "800x600_30.00" "3840x2160_25.00" "2560x1440_25.00" "1920x1080_25.00" "1600x900_25.00" "1368x768_25.00" "800x1600_30.00" "800x1600_25.00"
Modes "2560x1440_60.00" "1920x1080_60.00" "1920x1200_60.00" "1440x900_60.00" "1280x720_60.00" "1152x648_60.00" "1024x768_60.00" "1024x576_60.00" "960x720_60.00" "800x600_60.00" "2560x1440_30.00" "1920x1080_30.00" "1920x1200_30.00" "1440x900_30.00" "1368x768_30.00" "1280x720_30.00" "1152x648_30.00" "1024x768_30.00" "1024x576_30.00" "960x720_30.00" "800x600_30.00" "800x1600_30.00" "3840x2160_25.00" "2560x1440_25.00" "1920x1080_25.00" "1920x1200_25.00" "1600x900_25.00" "1440x900_25.00" "1368x768_25.00" "1024x768_25.00" "800x1600_25.00" "2560x1440_10.00" "1920x1080_10.00" "1920x1200_10.00" "1440x900_10.00" "1024x768_10.00"
EndSubSection
EndSection

Expand Down
12 changes: 9 additions & 3 deletions server/cmd/api/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/onkernel/kernel-images/server/lib/devtoolsproxy"
"github.com/onkernel/kernel-images/server/lib/logger"
"github.com/onkernel/kernel-images/server/lib/nekoclient"
oapi "github.com/onkernel/kernel-images/server/lib/oapi"
"github.com/onkernel/kernel-images/server/lib/recorder"
"github.com/onkernel/kernel-images/server/lib/scaletozero"
Expand All @@ -29,22 +30,26 @@ type ApiService struct {
procMu sync.RWMutex
procs map[string]*processHandle

// Neko authenticated client
nekoAuthClient *nekoclient.AuthClient

// DevTools upstream manager (Chromium supervisord log tailer)
upstreamMgr *devtoolsproxy.UpstreamManager

stz scaletozero.Controller
stz scaletozero.Controller
}

var _ oapi.StrictServerInterface = (*ApiService)(nil)

func New(recordManager recorder.RecordManager, factory recorder.FFmpegRecorderFactory, upstreamMgr *devtoolsproxy.UpstreamManager, stz scaletozero.Controller) (*ApiService, error) {
func New(recordManager recorder.RecordManager, factory recorder.FFmpegRecorderFactory, upstreamMgr *devtoolsproxy.UpstreamManager, stz scaletozero.Controller, nekoAuthClient *nekoclient.AuthClient) (*ApiService, error) {
switch {
case recordManager == nil:
return nil, fmt.Errorf("recordManager cannot be nil")
case factory == nil:
return nil, fmt.Errorf("factory cannot be nil")
case upstreamMgr == nil:
return nil, fmt.Errorf("upstreamMgr cannot be nil")
case nekoAuthClient == nil:
return nil, fmt.Errorf("nekoAuthClient cannot be nil")
}

return &ApiService{
Expand All @@ -55,6 +60,7 @@ func New(recordManager recorder.RecordManager, factory recorder.FFmpegRecorderFa
procs: make(map[string]*processHandle),
upstreamMgr: upstreamMgr,
stz: stz,
nekoAuthClient: nekoAuthClient,
}, nil
}

Expand Down
29 changes: 19 additions & 10 deletions server/cmd/api/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"log/slog"

"github.com/onkernel/kernel-images/server/lib/devtoolsproxy"
"github.com/onkernel/kernel-images/server/lib/nekoclient"
oapi "github.com/onkernel/kernel-images/server/lib/oapi"
"github.com/onkernel/kernel-images/server/lib/recorder"
"github.com/onkernel/kernel-images/server/lib/scaletozero"
Expand All @@ -24,7 +25,7 @@ func TestApiService_StartRecording(t *testing.T) {

t.Run("success", func(t *testing.T) {
mgr := recorder.NewFFmpegManager()
svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController())
svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t))
require.NoError(t, err)

resp, err := svc.StartRecording(ctx, oapi.StartRecordingRequestObject{})
Expand All @@ -38,7 +39,7 @@ func TestApiService_StartRecording(t *testing.T) {

t.Run("already recording", func(t *testing.T) {
mgr := recorder.NewFFmpegManager()
svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController())
svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t))
require.NoError(t, err)

// First start should succeed
Expand All @@ -53,7 +54,7 @@ func TestApiService_StartRecording(t *testing.T) {

t.Run("custom ids don't collide", func(t *testing.T) {
mgr := recorder.NewFFmpegManager()
svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController())
svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t))
require.NoError(t, err)

for i := 0; i < 5; i++ {
Expand Down Expand Up @@ -86,7 +87,7 @@ func TestApiService_StopRecording(t *testing.T) {

t.Run("no active recording", func(t *testing.T) {
mgr := recorder.NewFFmpegManager()
svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController())
svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t))
require.NoError(t, err)

resp, err := svc.StopRecording(ctx, oapi.StopRecordingRequestObject{})
Expand All @@ -99,7 +100,7 @@ func TestApiService_StopRecording(t *testing.T) {
rec := &mockRecorder{id: "default", isRecordingFlag: true}
require.NoError(t, mgr.RegisterRecorder(ctx, rec), "failed to register recorder")

svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController())
svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t))
require.NoError(t, err)
resp, err := svc.StopRecording(ctx, oapi.StopRecordingRequestObject{})
require.NoError(t, err)
Expand All @@ -114,7 +115,7 @@ func TestApiService_StopRecording(t *testing.T) {

force := true
req := oapi.StopRecordingRequestObject{Body: &oapi.StopRecordingJSONRequestBody{ForceStop: &force}}
svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController())
svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t))
require.NoError(t, err)
resp, err := svc.StopRecording(ctx, req)
require.NoError(t, err)
Expand All @@ -128,7 +129,7 @@ func TestApiService_DownloadRecording(t *testing.T) {

t.Run("not found", func(t *testing.T) {
mgr := recorder.NewFFmpegManager()
svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController())
svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t))
require.NoError(t, err)
resp, err := svc.DownloadRecording(ctx, oapi.DownloadRecordingRequestObject{})
require.NoError(t, err)
Expand All @@ -148,7 +149,7 @@ func TestApiService_DownloadRecording(t *testing.T) {
rec := &mockRecorder{id: "default", isRecordingFlag: true, recordingData: randomBytes(minRecordingSizeInBytes - 1)}
require.NoError(t, mgr.RegisterRecorder(ctx, rec), "failed to register recorder")

svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController())
svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t))
require.NoError(t, err)
// will return a 202 when the recording is too small
resp, err := svc.DownloadRecording(ctx, oapi.DownloadRecordingRequestObject{})
Expand Down Expand Up @@ -178,7 +179,7 @@ func TestApiService_DownloadRecording(t *testing.T) {
rec := &mockRecorder{id: "default", recordingData: data}
require.NoError(t, mgr.RegisterRecorder(ctx, rec), "failed to register recorder")

svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController())
svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t))
require.NoError(t, err)
resp, err := svc.DownloadRecording(ctx, oapi.DownloadRecordingRequestObject{})
require.NoError(t, err)
Expand All @@ -198,7 +199,7 @@ func TestApiService_Shutdown(t *testing.T) {
rec := &mockRecorder{id: "default", isRecordingFlag: true}
require.NoError(t, mgr.RegisterRecorder(ctx, rec), "failed to register recorder")

svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController())
svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t))
require.NoError(t, err)

require.NoError(t, svc.Shutdown(ctx))
Expand Down Expand Up @@ -293,3 +294,11 @@ func newTestUpstreamManager() *devtoolsproxy.UpstreamManager {
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
return devtoolsproxy.NewUpstreamManager("", logger)
}

func newMockNekoClient(t *testing.T) *nekoclient.AuthClient {
// Create a mock client that won't actually connect to anything
// We use a dummy URL since tests don't need real Neko connectivity
client, err := nekoclient.NewAuthClient("http://localhost:9999", "admin", "admin")
require.NoError(t, err)
return client
}
Loading
Loading