556 lines
14 KiB
Go
556 lines
14 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"overlord-client/cmd/agent/config"
|
|
rt "overlord-client/cmd/agent/runtime"
|
|
"overlord-client/cmd/agent/wire"
|
|
|
|
"github.com/vmihailenco/msgpack/v5"
|
|
"nhooyr.io/websocket"
|
|
)
|
|
|
|
type testWriter struct {
|
|
msgs [][]byte
|
|
}
|
|
|
|
func (w *testWriter) Write(ctx context.Context, messageType websocket.MessageType, p []byte) error {
|
|
w.msgs = append(w.msgs, append([]byte(nil), p...))
|
|
return nil
|
|
}
|
|
|
|
func TestHandleFileList(t *testing.T) {
|
|
|
|
tmpDir := t.TempDir()
|
|
|
|
testFile1 := filepath.Join(tmpDir, "test1.txt")
|
|
testFile2 := filepath.Join(tmpDir, "test2.txt")
|
|
testDir := filepath.Join(tmpDir, "testdir")
|
|
|
|
if err := os.WriteFile(testFile1, []byte("content1"), 0644); err != nil {
|
|
t.Fatalf("Failed to create test file: %v", err)
|
|
}
|
|
if err := os.WriteFile(testFile2, []byte("content2"), 0644); err != nil {
|
|
t.Fatalf("Failed to create test file: %v", err)
|
|
}
|
|
if err := os.Mkdir(testDir, 0755); err != nil {
|
|
t.Fatalf("Failed to create test directory: %v", err)
|
|
}
|
|
|
|
writer := &testWriter{}
|
|
env := &rt.Env{
|
|
Conn: writer,
|
|
Cfg: config.Config{},
|
|
}
|
|
|
|
ctx := context.Background()
|
|
cmdID := "test-cmd-1"
|
|
|
|
if err := HandleFileList(ctx, env, cmdID, tmpDir); err != nil {
|
|
t.Fatalf("HandleFileList failed: %v", err)
|
|
}
|
|
|
|
if len(writer.msgs) != 1 {
|
|
t.Fatalf("Expected 1 message, got %d", len(writer.msgs))
|
|
}
|
|
|
|
var result wire.FileListResult
|
|
if err := msgpack.Unmarshal(writer.msgs[0], &result); err != nil {
|
|
t.Fatalf("Failed to unmarshal result: %v", err)
|
|
}
|
|
|
|
if result.Type != "file_list_result" {
|
|
t.Errorf("Expected type 'file_list_result', got '%s'", result.Type)
|
|
}
|
|
if result.CommandID != cmdID {
|
|
t.Errorf("Expected CommandID '%s', got '%s'", cmdID, result.CommandID)
|
|
}
|
|
if len(result.Entries) != 3 {
|
|
t.Errorf("Expected 3 entries, got %d", len(result.Entries))
|
|
}
|
|
|
|
names := make(map[string]bool)
|
|
for _, entry := range result.Entries {
|
|
names[entry.Name] = entry.IsDir
|
|
}
|
|
|
|
if len(names) != 3 {
|
|
t.Logf("Found entries: %v", names)
|
|
}
|
|
|
|
hasTest1 := false
|
|
hasTest2 := false
|
|
hasTestDir := false
|
|
for name, isDir := range names {
|
|
if name == "test1.txt" {
|
|
hasTest1 = true
|
|
}
|
|
if name == "test2.txt" {
|
|
hasTest2 = true
|
|
}
|
|
if isDir {
|
|
hasTestDir = true
|
|
}
|
|
}
|
|
|
|
if !hasTest1 || !hasTest2 || !hasTestDir {
|
|
t.Errorf("Missing expected entries - test1:%v test2:%v testdir:%v", hasTest1, hasTest2, hasTestDir)
|
|
}
|
|
}
|
|
|
|
func TestHandleFileList_InvalidPath(t *testing.T) {
|
|
writer := &testWriter{}
|
|
env := &rt.Env{
|
|
Conn: writer,
|
|
Cfg: config.Config{},
|
|
}
|
|
|
|
ctx := context.Background()
|
|
cmdID := "test-cmd-2"
|
|
|
|
invalidPath := filepath.Join(t.TempDir(), "nonexistent")
|
|
if err := HandleFileList(ctx, env, cmdID, invalidPath); err != nil {
|
|
t.Fatalf("HandleFileList should not return error: %v", err)
|
|
}
|
|
|
|
if len(writer.msgs) != 1 {
|
|
t.Fatalf("Expected 1 message, got %d", len(writer.msgs))
|
|
}
|
|
|
|
var result wire.FileListResult
|
|
if err := msgpack.Unmarshal(writer.msgs[0], &result); err != nil {
|
|
t.Fatalf("Failed to unmarshal result: %v", err)
|
|
}
|
|
|
|
if result.Error == "" {
|
|
t.Error("Expected error message for invalid path")
|
|
}
|
|
}
|
|
|
|
func TestHandleFileDownload(t *testing.T) {
|
|
|
|
tmpDir := t.TempDir()
|
|
testFile := filepath.Join(tmpDir, "download_test.txt")
|
|
testContent := []byte("This is test content for download")
|
|
|
|
if err := os.WriteFile(testFile, testContent, 0644); err != nil {
|
|
t.Fatalf("Failed to create test file: %v", err)
|
|
}
|
|
|
|
writer := &testWriter{}
|
|
env := &rt.Env{
|
|
Conn: writer,
|
|
Cfg: config.Config{},
|
|
}
|
|
|
|
ctx := context.Background()
|
|
cmdID := "test-cmd-3"
|
|
|
|
if err := HandleFileDownload(ctx, env, cmdID, testFile); err != nil {
|
|
t.Fatalf("HandleFileDownload failed: %v", err)
|
|
}
|
|
|
|
if len(writer.msgs) == 0 {
|
|
t.Fatal("Expected at least 1 message")
|
|
}
|
|
|
|
var download wire.FileDownload
|
|
if err := msgpack.Unmarshal(writer.msgs[0], &download); err != nil {
|
|
t.Fatalf("Failed to unmarshal download message: %v", err)
|
|
}
|
|
|
|
if download.Type != "file_download" {
|
|
t.Errorf("Expected type 'file_download', got '%s'", download.Type)
|
|
}
|
|
if download.CommandID != cmdID {
|
|
t.Errorf("Expected CommandID '%s', got '%s'", cmdID, download.CommandID)
|
|
}
|
|
if download.Total != int64(len(testContent)) {
|
|
t.Errorf("Expected total size %d, got %d", len(testContent), download.Total)
|
|
}
|
|
|
|
var assembled []byte
|
|
for _, msg := range writer.msgs {
|
|
var chunk wire.FileDownload
|
|
if err := msgpack.Unmarshal(msg, &chunk); err != nil {
|
|
t.Fatalf("Failed to unmarshal chunk: %v", err)
|
|
}
|
|
assembled = append(assembled, chunk.Data...)
|
|
}
|
|
|
|
if string(assembled) != string(testContent) {
|
|
t.Errorf("Downloaded content mismatch:\ngot: %s\nwant: %s", string(assembled), string(testContent))
|
|
}
|
|
}
|
|
|
|
func TestHandleFileDelete(t *testing.T) {
|
|
|
|
tmpDir := t.TempDir()
|
|
testFile := filepath.Join(tmpDir, "delete_test.txt")
|
|
|
|
if err := os.WriteFile(testFile, []byte("delete me"), 0644); err != nil {
|
|
t.Fatalf("Failed to create test file: %v", err)
|
|
}
|
|
|
|
writer := &testWriter{}
|
|
env := &rt.Env{
|
|
Conn: writer,
|
|
Cfg: config.Config{},
|
|
}
|
|
|
|
ctx := context.Background()
|
|
cmdID := "test-cmd-4"
|
|
|
|
if err := HandleFileDelete(ctx, env, cmdID, testFile); err != nil {
|
|
t.Fatalf("HandleFileDelete failed: %v", err)
|
|
}
|
|
|
|
if _, err := os.Stat(testFile); !os.IsNotExist(err) {
|
|
t.Error("File should have been deleted")
|
|
}
|
|
|
|
if len(writer.msgs) != 1 {
|
|
t.Fatalf("Expected 1 message, got %d", len(writer.msgs))
|
|
}
|
|
|
|
var result wire.CommandResult
|
|
if err := msgpack.Unmarshal(writer.msgs[0], &result); err != nil {
|
|
t.Fatalf("Failed to unmarshal result: %v", err)
|
|
}
|
|
|
|
if !result.OK {
|
|
t.Errorf("Expected OK=true, got false with message: %s", result.Message)
|
|
}
|
|
}
|
|
|
|
func TestHandleFileMkdir(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
newDir := filepath.Join(tmpDir, "newdir")
|
|
|
|
writer := &testWriter{}
|
|
env := &rt.Env{
|
|
Conn: writer,
|
|
Cfg: config.Config{},
|
|
}
|
|
|
|
ctx := context.Background()
|
|
cmdID := "test-cmd-5"
|
|
|
|
if err := HandleFileMkdir(ctx, env, cmdID, newDir); err != nil {
|
|
t.Fatalf("HandleFileMkdir failed: %v", err)
|
|
}
|
|
|
|
info, err := os.Stat(newDir)
|
|
if err != nil {
|
|
t.Fatalf("Directory should have been created: %v", err)
|
|
}
|
|
if !info.IsDir() {
|
|
t.Error("Created path should be a directory")
|
|
}
|
|
|
|
if len(writer.msgs) != 1 {
|
|
t.Fatalf("Expected 1 message, got %d", len(writer.msgs))
|
|
}
|
|
|
|
var result wire.CommandResult
|
|
if err := msgpack.Unmarshal(writer.msgs[0], &result); err != nil {
|
|
t.Fatalf("Failed to unmarshal result: %v", err)
|
|
}
|
|
|
|
if !result.OK {
|
|
t.Errorf("Expected OK=true, got false with message: %s", result.Message)
|
|
}
|
|
}
|
|
|
|
func TestHandleFileUpload(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
uploadPath := filepath.Join(tmpDir, "uploaded.txt")
|
|
uploadContent := []byte("This is uploaded content")
|
|
|
|
writer := &testWriter{}
|
|
env := &rt.Env{
|
|
Conn: writer,
|
|
Cfg: config.Config{},
|
|
}
|
|
|
|
ctx := context.Background()
|
|
cmdID := "test-cmd-6"
|
|
|
|
if err := HandleFileUpload(ctx, env, cmdID, uploadPath, uploadContent, 0, int64(len(uploadContent)), "test-transfer"); err != nil {
|
|
t.Fatalf("HandleFileUpload failed: %v", err)
|
|
}
|
|
|
|
content, err := os.ReadFile(uploadPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read uploaded file: %v", err)
|
|
}
|
|
|
|
if string(content) != string(uploadContent) {
|
|
t.Errorf("Upload content mismatch:\ngot: %s\nwant: %s", string(content), string(uploadContent))
|
|
}
|
|
|
|
if len(writer.msgs) != 1 {
|
|
t.Fatalf("Expected 1 message, got %d", len(writer.msgs))
|
|
}
|
|
|
|
var result wire.FileUploadResult
|
|
if err := msgpack.Unmarshal(writer.msgs[0], &result); err != nil {
|
|
t.Fatalf("Failed to unmarshal result: %v", err)
|
|
}
|
|
|
|
if !result.OK {
|
|
t.Errorf("Expected OK=true, got false with error: %s", result.Error)
|
|
}
|
|
}
|
|
|
|
func TestHandleFileDownload_Chunked(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
filePath := filepath.Join(tmpDir, "chunked.bin")
|
|
data := bytes.Repeat([]byte("a"), maxChunkSize*2+123)
|
|
|
|
if err := os.WriteFile(filePath, data, 0644); err != nil {
|
|
t.Fatalf("Failed to create test file: %v", err)
|
|
}
|
|
|
|
writer := &testWriter{}
|
|
env := &rt.Env{Conn: writer, Cfg: config.Config{}}
|
|
ctx := context.Background()
|
|
|
|
if err := HandleFileDownload(ctx, env, "cmd-chunked-dl", filePath); err != nil {
|
|
t.Fatalf("HandleFileDownload failed: %v", err)
|
|
}
|
|
|
|
if len(writer.msgs) < 2 {
|
|
t.Fatalf("Expected multiple chunks, got %d", len(writer.msgs))
|
|
}
|
|
|
|
var assembled []byte
|
|
var chunksTotal int
|
|
for _, msg := range writer.msgs {
|
|
var chunk wire.FileDownload
|
|
if err := msgpack.Unmarshal(msg, &chunk); err != nil {
|
|
t.Fatalf("Failed to unmarshal chunk: %v", err)
|
|
}
|
|
assembled = append(assembled, chunk.Data...)
|
|
chunksTotal = chunk.ChunksTotal
|
|
if chunk.Total != int64(len(data)) {
|
|
t.Fatalf("Expected total %d, got %d", len(data), chunk.Total)
|
|
}
|
|
}
|
|
|
|
if chunksTotal < 2 {
|
|
t.Fatalf("Expected chunksTotal >= 2, got %d", chunksTotal)
|
|
}
|
|
if !bytes.Equal(assembled, data) {
|
|
t.Fatalf("Downloaded content mismatch")
|
|
}
|
|
}
|
|
|
|
func TestHandleFileUpload_Chunked(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
uploadPath := filepath.Join(tmpDir, "uploaded.bin")
|
|
data := bytes.Repeat([]byte("b"), maxChunkSize*2+77)
|
|
|
|
writer := &testWriter{}
|
|
env := &rt.Env{Conn: writer, Cfg: config.Config{}}
|
|
ctx := context.Background()
|
|
cmdID := "cmd-chunked-ul"
|
|
transferID := "transfer-123"
|
|
|
|
chunks := [][]byte{
|
|
data[:maxChunkSize],
|
|
data[maxChunkSize : maxChunkSize*2],
|
|
data[maxChunkSize*2:],
|
|
}
|
|
|
|
offset := int64(0)
|
|
for _, chunk := range chunks {
|
|
if err := HandleFileUpload(ctx, env, cmdID, uploadPath, chunk, offset, int64(len(data)), transferID); err != nil {
|
|
t.Fatalf("HandleFileUpload failed: %v", err)
|
|
}
|
|
offset += int64(len(chunk))
|
|
}
|
|
|
|
content, err := os.ReadFile(uploadPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read uploaded file: %v", err)
|
|
}
|
|
if !bytes.Equal(content, data) {
|
|
t.Fatalf("Uploaded content mismatch")
|
|
}
|
|
|
|
if len(writer.msgs) != len(chunks) {
|
|
t.Fatalf("Expected %d messages, got %d", len(chunks), len(writer.msgs))
|
|
}
|
|
}
|
|
|
|
func TestHandleFileUpload_InvalidChunk(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
uploadPath := filepath.Join(tmpDir, "bad.bin")
|
|
|
|
writer := &testWriter{}
|
|
env := &rt.Env{Conn: writer, Cfg: config.Config{}}
|
|
ctx := context.Background()
|
|
|
|
if err := HandleFileUpload(ctx, env, "cmd-bad", uploadPath, []byte("bad"), 5, 4, "bad-transfer"); err != nil {
|
|
t.Fatalf("HandleFileUpload failed: %v", err)
|
|
}
|
|
|
|
if len(writer.msgs) != 1 {
|
|
t.Fatalf("Expected 1 message, got %d", len(writer.msgs))
|
|
}
|
|
|
|
var result wire.FileUploadResult
|
|
if err := msgpack.Unmarshal(writer.msgs[0], &result); err != nil {
|
|
t.Fatalf("Failed to unmarshal result: %v", err)
|
|
}
|
|
if result.OK {
|
|
t.Fatalf("Expected OK=false for invalid chunk")
|
|
}
|
|
}
|
|
|
|
func TestHandleFileZip(t *testing.T) {
|
|
|
|
tmpDir := t.TempDir()
|
|
sourceDir := filepath.Join(tmpDir, "source")
|
|
|
|
if err := os.Mkdir(sourceDir, 0755); err != nil {
|
|
t.Fatalf("Failed to create source dir: %v", err)
|
|
}
|
|
|
|
testFile := filepath.Join(sourceDir, "test.txt")
|
|
if err := os.WriteFile(testFile, []byte("zip me"), 0644); err != nil {
|
|
t.Fatalf("Failed to create test file: %v", err)
|
|
}
|
|
|
|
writer := &testWriter{}
|
|
env := &rt.Env{
|
|
Conn: writer,
|
|
Cfg: config.Config{},
|
|
}
|
|
|
|
ctx := context.Background()
|
|
cmdID := "test-cmd-7"
|
|
|
|
if err := HandleFileZip(ctx, env, cmdID, sourceDir); err != nil {
|
|
t.Fatalf("HandleFileZip failed: %v", err)
|
|
}
|
|
|
|
if len(writer.msgs) == 0 {
|
|
t.Fatal("Expected at least 1 message")
|
|
}
|
|
|
|
hasResult := false
|
|
hasDownload := false
|
|
|
|
for _, msg := range writer.msgs {
|
|
var testMsg map[string]interface{}
|
|
if err := msgpack.Unmarshal(msg, &testMsg); err == nil {
|
|
msgType, _ := testMsg["type"].(string)
|
|
if msgType == "command_result" {
|
|
hasResult = true
|
|
}
|
|
if msgType == "file_download" {
|
|
hasDownload = true
|
|
}
|
|
}
|
|
}
|
|
|
|
t.Logf("Has command_result: %v, has file_download: %v", hasResult, hasDownload)
|
|
|
|
if !hasResult && !hasDownload {
|
|
t.Error("Expected either command_result or file_download message")
|
|
}
|
|
}
|
|
|
|
func TestHandleFileUploadHTTP(t *testing.T) {
|
|
data := []byte("http-upload-payload")
|
|
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if got := r.Header.Get("x-overlord-client-id"); got != "client-abc" {
|
|
t.Fatalf("expected x-overlord-client-id client-abc, got %q", got)
|
|
}
|
|
if got := r.Header.Get("x-agent-token"); got != "agent-token" {
|
|
t.Fatalf("expected x-agent-token agent-token, got %q", got)
|
|
}
|
|
_, _ = w.Write(data)
|
|
}))
|
|
defer ts.Close()
|
|
|
|
tmpDir := t.TempDir()
|
|
destPath := filepath.Join(tmpDir, "uploaded-http.bin")
|
|
|
|
writer := &testWriter{}
|
|
env := &rt.Env{
|
|
Conn: writer,
|
|
Cfg: config.Config{
|
|
ID: "client-abc",
|
|
AgentToken: "agent-token",
|
|
TLSInsecureSkipVerify: true,
|
|
},
|
|
}
|
|
|
|
if err := HandleFileUploadHTTP(context.Background(), env, "cmd-http-upload", destPath, ts.URL+"/file", int64(len(data))); err != nil {
|
|
t.Fatalf("HandleFileUploadHTTP failed: %v", err)
|
|
}
|
|
|
|
content, err := os.ReadFile(destPath)
|
|
if err != nil {
|
|
t.Fatalf("failed reading uploaded file: %v", err)
|
|
}
|
|
if !bytes.Equal(content, data) {
|
|
t.Fatalf("uploaded content mismatch")
|
|
}
|
|
|
|
if len(writer.msgs) != 1 {
|
|
t.Fatalf("expected 1 message, got %d", len(writer.msgs))
|
|
}
|
|
var result wire.CommandResult
|
|
if err := msgpack.Unmarshal(writer.msgs[0], &result); err != nil {
|
|
t.Fatalf("failed to unmarshal command result: %v", err)
|
|
}
|
|
if !result.OK {
|
|
t.Fatalf("expected OK=true, got false: %s", result.Message)
|
|
}
|
|
}
|
|
|
|
func TestHandleFileUploadHTTP_SizeMismatch(t *testing.T) {
|
|
data := []byte("small")
|
|
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write(data)
|
|
}))
|
|
defer ts.Close()
|
|
|
|
tmpDir := t.TempDir()
|
|
destPath := filepath.Join(tmpDir, "uploaded-http-mismatch.bin")
|
|
|
|
writer := &testWriter{}
|
|
env := &rt.Env{
|
|
Conn: writer,
|
|
Cfg: config.Config{TLSInsecureSkipVerify: true},
|
|
}
|
|
|
|
if err := HandleFileUploadHTTP(context.Background(), env, "cmd-http-upload-mismatch", destPath, ts.URL+"/file", int64(len(data)+1)); err != nil {
|
|
t.Fatalf("HandleFileUploadHTTP failed: %v", err)
|
|
}
|
|
|
|
if len(writer.msgs) != 1 {
|
|
t.Fatalf("expected 1 message, got %d", len(writer.msgs))
|
|
}
|
|
var result wire.CommandResult
|
|
if err := msgpack.Unmarshal(writer.msgs[0], &result); err != nil {
|
|
t.Fatalf("failed to unmarshal command result: %v", err)
|
|
}
|
|
if result.OK {
|
|
t.Fatalf("expected OK=false for size mismatch")
|
|
}
|
|
}
|