feat: add FTP and SFTP upload support

This commit is contained in:
2026-07-12 10:04:21 +08:00
parent f1998fb23c
commit dd12521be2
22 changed files with 1466 additions and 44 deletions
+131
View File
@@ -0,0 +1,131 @@
package application
import (
"context"
"errors"
"strings"
"testing"
"github.com/mmmy/snapgo/internal/domain"
)
type fakeFTPUploader struct {
path string
data []byte
err error
}
func (f *fakeFTPUploader) Upload(_ context.Context, remoteRelPath string, data []byte) error {
f.path = remoteRelPath
f.data = append([]byte(nil), data...)
return f.err
}
func TestCaptureAndFTPServiceUploadsAndCopiesFTPPath(t *testing.T) {
uploader := &fakeFTPUploader{}
clip := &fakeClipboard{}
notifier := &fakeNotifier{}
svc := &CaptureAndFTPService{
Uploader: uploader,
Clipboard: clip,
Notifier: notifier,
Cfg: domain.FTPConfig{
Protocol: domain.FTPProtocolFTP,
PathPrefix: "snapgo/",
},
}
if err := svc.ExecuteWithBytes(context.Background(), []byte("png")); err != nil {
t.Fatalf("upload FTP screenshot: %v", err)
}
if string(uploader.data) != "png" {
t.Fatalf("expected PNG bytes, got %q", string(uploader.data))
}
if !strings.HasPrefix(uploader.path, "snapgo/") || !strings.HasSuffix(uploader.path, ".png") {
t.Fatalf("unexpected remote path %q", uploader.path)
}
if clip.text != "/"+uploader.path {
t.Fatalf("expected FTP root path copied, got %q", clip.text)
}
if notifier.success != clip.text {
t.Fatalf("expected success notification %q, got %q", clip.text, notifier.success)
}
}
func TestCaptureAndFTPServiceCopiesSFTPHomePath(t *testing.T) {
uploader := &fakeFTPUploader{}
clip := &fakeClipboard{}
svc := &CaptureAndFTPService{
Uploader: uploader,
Clipboard: clip,
Cfg: domain.FTPConfig{
Protocol: domain.FTPProtocolSFTP,
PathPrefix: "images",
},
}
if err := svc.ExecuteWithBytes(context.Background(), []byte("png")); err != nil {
t.Fatalf("upload SFTP screenshot: %v", err)
}
if clip.text != "~/"+uploader.path {
t.Fatalf("expected SFTP home path copied, got %q", clip.text)
}
}
func TestCaptureAndFTPServiceRejectsTraversalBeforeUpload(t *testing.T) {
uploader := &fakeFTPUploader{}
notifier := &fakeNotifier{}
svc := &CaptureAndFTPService{
Uploader: uploader,
Notifier: notifier,
Cfg: domain.FTPConfig{
Protocol: domain.FTPProtocolSFTP,
PathPrefix: "../../outside",
},
}
err := svc.ExecuteWithBytes(context.Background(), []byte("png"))
if err == nil {
t.Fatal("expected traversal prefix to fail")
}
if uploader.path != "" {
t.Fatalf("uploader should not run, got path %q", uploader.path)
}
if notifier.failure == "" {
t.Fatal("expected failure notification")
}
}
func TestCaptureAndFTPServiceSurfacesUploadFailure(t *testing.T) {
want := errors.New("server unavailable")
uploader := &fakeFTPUploader{err: want}
clip := &fakeClipboard{}
notifier := &fakeNotifier{}
svc := &CaptureAndFTPService{
Uploader: uploader,
Clipboard: clip,
Notifier: notifier,
Cfg: domain.FTPConfig{Protocol: domain.FTPProtocolFTP},
}
err := svc.ExecuteWithBytes(context.Background(), []byte("png"))
if !errors.Is(err, want) {
t.Fatalf("expected %v, got %v", want, err)
}
if clip.text != "" {
t.Fatalf("clipboard should remain empty, got %q", clip.text)
}
if !strings.Contains(notifier.failure, "server unavailable") {
t.Fatalf("unexpected failure notification %q", notifier.failure)
}
}
func TestBuildFTPRemotePathNormalizesLoginRootMarkers(t *testing.T) {
remotePath, err := buildFTPRemotePath(" ~////snapgo/ ")
if err != nil {
t.Fatalf("build remote path: %v", err)
}
if strings.HasPrefix(remotePath, "/") || !strings.HasPrefix(remotePath, "snapgo/") {
t.Fatalf("expected relative snapgo path, got %q", remotePath)
}
}