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 TestCaptureAndFTPServiceCopiesConfiguredRootPath(t *testing.T) { uploader := &fakeFTPUploader{} clip := &fakeClipboard{} svc := &CaptureAndFTPService{ Uploader: uploader, Clipboard: clip, Cfg: domain.FTPConfig{ Protocol: domain.FTPProtocolSFTP, RootDirectory: "~/ftp/", PathPrefix: "images", }, } if err := svc.ExecuteWithBytes(context.Background(), []byte("png")); err != nil { t.Fatalf("upload SFTP screenshot: %v", err) } if clip.text != "~/ftp/"+uploader.path { t.Fatalf("expected configured SFTP root path copied, got %q", clip.text) } } func TestFTPShareTextJoinsConfiguredRootAndRemoteDirectory(t *testing.T) { const relPath = "snapgo/2026/07/20260713-110413-18lnbx.png" const want = "~/ftp/snapgo/2026/07/20260713-110413-18lnbx.png" if got := ftpShareText(domain.FTPProtocolSFTP, "~/ftp/", relPath); got != want { t.Fatalf("ftpShareText() = %q, want %q", got, want) } } func TestFTPShareTextUsesProtocolDefaultWhenRootDirectoryIsEmpty(t *testing.T) { const relPath = "snapgo/2026/07/image.png" tests := []struct { name string protocol string want string }{ {name: "FTP", protocol: domain.FTPProtocolFTP, want: "/" + relPath}, {name: "SFTP", protocol: domain.FTPProtocolSFTP, want: "~/" + relPath}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := ftpShareText(tt.protocol, "", relPath); got != tt.want { t.Fatalf("ftpShareText() = %q, want %q", got, tt.want) } }) } } 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) } }