Files
photocli/cmd/photoscli/main_test.go
T
Ein Anderssono 479c284dfc v0.2.4: stop export loop on Ctrl+C instead of flooding failures
- Add IsCancelled() to Bridge interface
- Check bridge.IsCancelled() before each export in serial/parallel/backupTree
- Parallel workers mark remaining slots as 'cancelled' instead of exporting
- Add photos_request_is_cancelled to ObjC and C stub
2026-06-11 21:44:55 +02:00

687 lines
20 KiB
Go

//go:build test
package main
import (
"bytes"
"fmt"
"strings"
"testing"
"gitea.k3s.k0.nu/tools/photocli/internal/photos"
)
type mockBridge struct {
accessErr error
albums []photos.Album
albumsErr error
assets []photos.Asset
assetsErr error
assetsByAlbum map[string][]photos.Asset
tree []photos.CollectionNode
treeErr error
exportPreviewFn func(string, string, int, int) (photos.ExportResult, error)
exportOrigFn func(string, string, int) (photos.ExportResult, error)
cancelled bool
}
func (m *mockBridge) RequestAccess() error { return m.accessErr }
func (m *mockBridge) ListAlbums() ([]photos.Album, error) { return m.albums, m.albumsErr }
func (m *mockBridge) ListAssets(albumID string) ([]photos.Asset, int, error) {
if m.assetsByAlbum != nil {
if assets, ok := m.assetsByAlbum[albumID]; ok {
return assets, len(assets), nil
}
return nil, 0, fmt.Errorf("album not found")
}
return m.assets, len(m.assets), m.assetsErr
}
func (m *mockBridge) ListTree() ([]photos.CollectionNode, error) { return m.tree, m.treeErr }
func (m *mockBridge) ExportPreview(assetID, out string, targetSize, index int) (photos.ExportResult, error) {
if m.exportPreviewFn != nil {
return m.exportPreviewFn(assetID, out, targetSize, index)
}
return photos.ExportResult{Filename: "0000_test.jpg", Size: 1024, Cloud: "local"}, nil
}
func (m *mockBridge) ExportOriginal(assetID, out string, index int) (photos.ExportResult, error) {
if m.exportOrigFn != nil {
return m.exportOrigFn(assetID, out, index)
}
return photos.ExportResult{Filename: "test.jpg", Size: 2048, Cloud: "cloud"}, nil
}
func (m *mockBridge) Cancel() { m.cancelled = true }
func (m *mockBridge) IsCancelled() bool { return m.cancelled }
func runWith(args []string, b photos.Bridge) (string, string, int) {
var out, err bytes.Buffer
rc := run(args, &out, &err, b)
return out.String(), err.String(), rc
}
func TestRunNoArgs(t *testing.T) {
_, stderr, rc := runWith(nil, &mockBridge{})
if rc != 1 {
t.Errorf("rc = %d, want 1", rc)
}
if !strings.Contains(stderr, "photoscli") {
t.Errorf("stderr should contain usage, got: %s", stderr)
}
}
func TestRunHelp(t *testing.T) {
for _, cmd := range []string{"help", "--help", "-h"} {
_, stderr, rc := runWith([]string{cmd}, &mockBridge{})
if rc != 0 {
t.Errorf("%s: rc = %d, want 0", cmd, rc)
}
if !strings.Contains(stderr, "photoscli") {
t.Errorf("%s: stderr should contain usage", cmd)
}
}
}
func TestRunVersion(t *testing.T) {
for _, cmd := range []string{"version", "--version", "-v"} {
out, _, rc := runWith([]string{cmd}, &mockBridge{})
if rc != 0 {
t.Errorf("%s: rc = %d, want 0", cmd, rc)
}
if out == "" {
t.Errorf("%s: output is empty", cmd)
}
}
}
func TestRunUnknownCommand(t *testing.T) {
_, stderr, rc := runWith([]string{"foo"}, &mockBridge{})
if rc != 1 {
t.Errorf("rc = %d, want 1", rc)
}
if !strings.Contains(stderr, "unknown command: foo") {
t.Errorf("stderr = %q", stderr)
}
}
func TestCmdAlbumsSuccess(t *testing.T) {
b := &mockBridge{
albums: []photos.Album{{ID: "a1", Title: "Vacation"}, {ID: "a2", Title: "Work"}},
}
out, _, rc := runWith([]string{"albums"}, b)
if rc != 0 {
t.Errorf("rc = %d", rc)
}
expected := "a1\tVacation\na2\tWork\n"
if out != expected {
t.Errorf("out = %q, want %q", out, expected)
}
}
func TestCmdAlbumsAuthDenied(t *testing.T) {
b := &mockBridge{accessErr: fmt.Errorf("denied")}
_, stderr, rc := runWith([]string{"albums"}, b)
if rc != 1 {
t.Errorf("rc = %d, want 1", rc)
}
if !strings.Contains(stderr, "denied") {
t.Errorf("stderr = %q", stderr)
}
}
func TestCmdAlbumsBridgeError(t *testing.T) {
b := &mockBridge{albumsErr: fmt.Errorf("boom")}
_, stderr, rc := runWith([]string{"albums"}, b)
if rc != 1 {
t.Errorf("rc = %d, want 1", rc)
}
if !strings.Contains(stderr, "boom") {
t.Errorf("stderr = %q", stderr)
}
}
func TestCmdAlbumsEmpty(t *testing.T) {
b := &mockBridge{albums: []photos.Album{}}
out, _, rc := runWith([]string{"albums"}, b)
if rc != 0 {
t.Errorf("rc = %d", rc)
}
if out != "" {
t.Errorf("out = %q, want empty", out)
}
}
func TestCmdPhotosMissingAlbumID(t *testing.T) {
_, stderr, rc := runWith([]string{"photos"}, &mockBridge{})
if rc != 1 {
t.Errorf("rc = %d, want 1", rc)
}
if !strings.Contains(stderr, "--album-id is required") {
t.Errorf("stderr = %q", stderr)
}
}
func TestCmdPhotosSuccess(t *testing.T) {
b := &mockBridge{
assets: []photos.Asset{{ID: "as1", Filename: "IMG_0001.JPG", Cloud: "local"}, {ID: "as2", Filename: "IMG_0002.JPG", Cloud: "cloud"}},
}
out, _, rc := runWith([]string{"photos", "--album-id", "x"}, b)
if rc != 0 {
t.Errorf("rc = %d", rc)
}
expected := "as1\tIMG_0001.JPG\tlocal\nas2\tIMG_0002.JPG\tcloud\n"
if out != expected {
t.Errorf("out = %q, want %q", out, expected)
}
}
func TestCmdPhotosAuthDenied(t *testing.T) {
b := &mockBridge{accessErr: fmt.Errorf("nope")}
_, stderr, rc := runWith([]string{"photos", "--album-id", "x"}, b)
if rc != 1 {
t.Errorf("rc = %d", rc)
}
if !strings.Contains(stderr, "nope") {
t.Errorf("stderr = %q", stderr)
}
}
func TestCmdPhotosBridgeError(t *testing.T) {
b := &mockBridge{albumsErr: fmt.Errorf("fail")}
_, stderr, rc := runWith([]string{"photos", "--album-id", "x"}, b)
if rc != 1 {
t.Errorf("rc = %d", rc)
}
if !strings.Contains(stderr, "fail") {
t.Errorf("stderr = %q", stderr)
}
}
func TestCmdTreeSuccess(t *testing.T) {
b := &mockBridge{tree: []photos.CollectionNode{
{Name: "Trips", Kind: "folder", Children: []photos.CollectionNode{{Name: "Italy 2024", Kind: "folder", Children: []photos.CollectionNode{{Name: "Venice", Kind: "album"}}}}},
{Name: "Favorites", Kind: "album"},
}}
out, _, rc := runWith([]string{"tree"}, b)
if rc != 0 {
t.Errorf("rc = %d", rc)
}
expected := "Trips\n Italy 2024\n Venice\nFavorites\n"
if out != expected {
t.Errorf("out = %q, want %q", out, expected)
}
}
func TestCmdTreeAuthDenied(t *testing.T) {
b := &mockBridge{accessErr: fmt.Errorf("denied")}
_, stderr, rc := runWith([]string{"tree"}, b)
if rc != 1 {
t.Errorf("rc = %d, want 1", rc)
}
if !strings.Contains(stderr, "denied") {
t.Errorf("stderr = %q", stderr)
}
}
func TestCmdTreeBridgeError(t *testing.T) {
b := &mockBridge{treeErr: fmt.Errorf("boom")}
_, stderr, rc := runWith([]string{"tree"}, b)
if rc != 1 {
t.Errorf("rc = %d, want 1", rc)
}
if !strings.Contains(stderr, "boom") {
t.Errorf("stderr = %q", stderr)
}
}
func TestCmdBackupAllPreviewSuccess(t *testing.T) {
b := &mockBridge{
tree: []photos.CollectionNode{{Name: "Trips", Kind: "folder", Children: []photos.CollectionNode{{ID: "a1", Name: "Venice", Kind: "album"}}}, {ID: "a2", Name: "Favorites", Kind: "album"}},
assetsByAlbum: map[string][]photos.Asset{
"a1": {{ID: "as1", Filename: "img1.jpg"}},
"a2": {{ID: "as2", Filename: "img2.jpg"}},
},
}
_, stderr, rc := runWith([]string{"backup-all", "--out", "/backup", "--size", "2048"}, b)
if rc != 0 {
t.Fatalf("rc = %d, stderr = %q", rc, stderr)
}
if !strings.Contains(stderr, "exported 2 preview files across 2 albums to /backup") {
t.Errorf("stderr = %q", stderr)
}
if strings.Contains(stderr, "failed") {
t.Errorf("unexpected failed count in stderr = %q", stderr)
}
}
func TestCmdBackupAllOriginalsSuccess(t *testing.T) {
b := &mockBridge{
tree: []photos.CollectionNode{{Name: "Trips", Kind: "folder", Children: []photos.CollectionNode{{ID: "a1", Name: "Venice", Kind: "album"}}}},
assetsByAlbum: map[string][]photos.Asset{
"a1": {{ID: "as1", Filename: "img.jpg"}, {ID: "as2", Filename: "img2.jpg"}, {ID: "as3", Filename: "img3.jpg"}},
},
}
_, stderr, rc := runWith([]string{"backup-all", "--out", "/backup", "--originals", "--size", "bad"}, b)
if rc != 0 {
t.Fatalf("rc = %d, stderr = %q", rc, stderr)
}
if !strings.Contains(stderr, "exported 3 original files across 1 albums to /backup") {
t.Errorf("stderr = %q", stderr)
}
if strings.Contains(stderr, "failed") {
t.Errorf("unexpected failed count in stderr = %q", stderr)
}
}
func TestCmdBackupAllMissingOutDir(t *testing.T) {
_, stderr, rc := runWith([]string{"backup-all"}, &mockBridge{})
if rc != 1 {
t.Fatalf("rc = %d", rc)
}
if !strings.Contains(stderr, "--out is required") {
t.Errorf("stderr = %q", stderr)
}
}
func TestCmdBackupAllInvalidSize(t *testing.T) {
b := &mockBridge{tree: []photos.CollectionNode{}}
_, stderr, rc := runWith([]string{"backup-all", "--out", "/backup", "--size", "bad"}, b)
if rc != 1 {
t.Fatalf("rc = %d", rc)
}
if !strings.Contains(stderr, "--size must be a positive integer") {
t.Errorf("stderr = %q", stderr)
}
}
func TestCmdBackupAllTreeError(t *testing.T) {
b := &mockBridge{treeErr: fmt.Errorf("tree fail")}
_, stderr, rc := runWith([]string{"backup-all", "--out", "/backup"}, b)
if rc != 1 {
t.Fatalf("rc = %d", rc)
}
if !strings.Contains(stderr, "tree fail") {
t.Errorf("stderr = %q", stderr)
}
}
func TestCmdBackupAllExportError(t *testing.T) {
b := &mockBridge{
tree: []photos.CollectionNode{{ID: "a1", Name: "Venice", Kind: "album"}},
assetsByAlbum: map[string][]photos.Asset{
"a1": {{ID: "as1", Filename: "img.jpg"}},
},
exportPreviewFn: func(string, string, int, int) (photos.ExportResult, error) {
return photos.ExportResult{}, fmt.Errorf("disk full")
},
}
_, stderr, rc := runWith([]string{"backup-all", "--out", "/backup"}, b)
if rc != 0 {
t.Fatalf("rc = %d, stderr = %q", rc, stderr)
}
if !strings.Contains(stderr, "failed: img.jpg: disk full") {
t.Errorf("stderr should contain failure detail, got: %q", stderr)
}
if !strings.Contains(stderr, "(1 failed)") {
t.Errorf("stderr should contain failed count, got: %q", stderr)
}
}
func TestCmdExportMissingAlbumID(t *testing.T) {
_, stderr, rc := runWith([]string{"export", "--out", "/tmp"}, &mockBridge{})
if rc != 1 {
t.Errorf("rc = %d", rc)
}
if !strings.Contains(stderr, "--album-id is required") {
t.Errorf("stderr = %q", stderr)
}
}
func TestCmdExportMissingOutDir(t *testing.T) {
_, stderr, rc := runWith([]string{"export", "--album-id", "x"}, &mockBridge{})
if rc != 1 {
t.Errorf("rc = %d", rc)
}
if !strings.Contains(stderr, "--out is required") {
t.Errorf("stderr = %q", stderr)
}
}
func TestCmdExportInvalidSize(t *testing.T) {
_, stderr, rc := runWith([]string{"export", "--album-id", "x", "--out", "/tmp", "--size", "abc"}, &mockBridge{})
if rc != 1 {
t.Errorf("rc = %d", rc)
}
if !strings.Contains(stderr, "--size must be a positive integer") {
t.Errorf("stderr = %q", stderr)
}
}
func TestCmdExportNegativeSize(t *testing.T) {
_, stderr, rc := runWith([]string{"export", "--album-id", "x", "--out", "/tmp", "--size", "-5"}, &mockBridge{})
if rc != 1 {
t.Errorf("rc = %d", rc)
}
if !strings.Contains(stderr, "--size must be a positive integer") {
t.Errorf("stderr = %q", stderr)
}
}
func TestCmdExportSuccess(t *testing.T) {
b := &mockBridge{
assets: []photos.Asset{{ID: "a1", Filename: "img.jpg"}},
}
_, stderr, rc := runWith([]string{"export", "--album-id", "x", "--out", "/tmp/out"}, b)
if rc != 0 {
t.Errorf("rc = %d, stderr = %q", rc, stderr)
}
if !strings.Contains(stderr, "exported 1 photos to /tmp/out") {
t.Errorf("stderr = %q", stderr)
}
}
func TestCmdExportDefaultSize(t *testing.T) {
b := &mockBridge{
assets: []photos.Asset{{ID: "a1", Filename: "img.jpg"}},
}
_, stderr, rc := runWith([]string{"export", "--album-id", "x", "--out", "/tmp"}, b)
if rc != 0 {
t.Errorf("rc = %d", rc)
}
if !strings.Contains(stderr, "exported 1") {
t.Errorf("stderr = %q", stderr)
}
}
func TestCmdExportAuthDenied(t *testing.T) {
b := &mockBridge{accessErr: fmt.Errorf("no")}
_, stderr, rc := runWith([]string{"export", "--album-id", "x", "--out", "/tmp"}, b)
if rc != 1 {
t.Errorf("rc = %d", rc)
}
if !strings.Contains(stderr, "no") {
t.Errorf("stderr = %q", stderr)
}
}
func TestCmdExportBridgeError(t *testing.T) {
b := &mockBridge{
assets: []photos.Asset{{ID: "a1", Filename: "img.jpg"}},
exportPreviewFn: func(string, string, int, int) (photos.ExportResult, error) {
return photos.ExportResult{}, fmt.Errorf("disk full")
},
}
_, stderr, rc := runWith([]string{"export", "--album-id", "x", "--out", "/tmp"}, b)
if rc != 1 {
t.Errorf("rc = %d, stderr = %q", rc, stderr)
}
if !strings.Contains(stderr, "all exports failed") {
t.Errorf("stderr = %q", stderr)
}
if !strings.Contains(stderr, "failed: img.jpg: disk full") {
t.Errorf("stderr should contain failure detail, got: %q", stderr)
}
}
func TestCmdExportOriginalsSuccess(t *testing.T) {
b := &mockBridge{
assets: []photos.Asset{{ID: "a1", Filename: "img.jpg"}},
}
_, stderr, rc := runWith([]string{"export", "--album-id", "x", "--out", "/tmp/out", "--originals"}, b)
if rc != 0 {
t.Errorf("rc = %d, stderr = %q", rc, stderr)
}
if !strings.Contains(stderr, "exported 1 original files to /tmp/out") {
t.Errorf("stderr = %q", stderr)
}
}
func TestCmdExportOriginalsIgnoresSizeValidation(t *testing.T) {
b := &mockBridge{
assets: []photos.Asset{{ID: "a1", Filename: "img.jpg"}},
}
_, stderr, rc := runWith([]string{"export", "--album-id", "x", "--out", "/tmp/out", "--originals", "--size", "abc"}, b)
if rc != 0 {
t.Errorf("rc = %d, stderr = %q", rc, stderr)
}
if strings.Contains(stderr, "--size must be a positive integer") {
t.Errorf("stderr = %q", stderr)
}
}
func TestCmdExportOriginalsBridgeError(t *testing.T) {
b := &mockBridge{
assets: []photos.Asset{{ID: "a1", Filename: "img.jpg"}},
exportOrigFn: func(string, string, int) (photos.ExportResult, error) {
return photos.ExportResult{}, fmt.Errorf("copy failed")
},
}
_, stderr, rc := runWith([]string{"export", "--album-id", "x", "--out", "/tmp/out", "--originals"}, b)
if rc != 1 {
t.Errorf("rc = %d, stderr = %q", rc, stderr)
}
if !strings.Contains(stderr, "all exports failed") {
t.Errorf("stderr = %q", stderr)
}
if !strings.Contains(stderr, "failed: img.jpg: copy failed") {
t.Errorf("stderr should contain failure detail, got: %q", stderr)
}
}
func TestFlagVal(t *testing.T) {
args := []string{"--album-id", "abc", "--out", "/tmp"}
if v := flagVal(args, "--album-id"); v != "abc" {
t.Errorf("got %q", v)
}
if v := flagVal(args, "--out"); v != "/tmp" {
t.Errorf("got %q", v)
}
if v := flagVal(args, "--missing"); v != "" {
t.Errorf("got %q, want empty", v)
}
}
func TestFlagValTrailing(t *testing.T) {
args := []string{"--album-id"}
if v := flagVal(args, "--album-id"); v != "" {
t.Errorf("got %q, want empty for trailing flag", v)
}
}
func TestFlagValWithDefault(t *testing.T) {
args := []string{"--size", "2048"}
if v := flagValWithDefault(args, "--size", "1024"); v != "2048" {
t.Errorf("got %q", v)
}
if v := flagValWithDefault(args, "--missing", "fallback"); v != "fallback" {
t.Errorf("got %q", v)
}
}
func TestHasFlag(t *testing.T) {
args := []string{"export", "--originals", "--album-id", "x"}
if !hasFlag(args, "--originals") {
t.Fatal("expected --originals to be found")
}
if hasFlag(args, "--missing") {
t.Fatal("did not expect missing flag")
}
}
func TestFlagValEmptyArgs(t *testing.T) {
if v := flagVal(nil, "--album-id"); v != "" {
t.Errorf("got %q", v)
}
if v := flagValWithDefault(nil, "--size", "1024"); v != "1024" {
t.Errorf("got %q", v)
}
}
func TestResolveAlbumIDDirectMatch(t *testing.T) {
b := &mockBridge{
assetsByAlbum: map[string][]photos.Asset{
"ABC/L0/001": {{ID: "a1", Filename: "img.jpg", Cloud: "local"}},
},
}
id, err := resolveAlbumID(b, "ABC/L0/001")
if err != nil {
t.Fatal(err)
}
if id != "ABC/L0/001" {
t.Errorf("got %q", id)
}
}
func TestResolveAlbumIDByName(t *testing.T) {
b := &mockBridge{
albums: []photos.Album{
{ID: "ABC/L0/001", Title: "DnD"},
{ID: "DEF/L0/001", Title: "Work"},
},
assetsByAlbum: map[string][]photos.Asset{
"ABC/L0/001": {{ID: "a1", Filename: "img.jpg", Cloud: "local"}},
},
}
id, err := resolveAlbumID(b, "DnD")
if err != nil {
t.Fatal(err)
}
if id != "ABC/L0/001" {
t.Errorf("got %q, want ABC/L0/001", id)
}
}
func TestResolveAlbumIDNotFound(t *testing.T) {
b := &mockBridge{
albums: []photos.Album{
{ID: "ABC/L0/001", Title: "DnD"},
},
assetsByAlbum: map[string][]photos.Asset{},
}
_, err := resolveAlbumID(b, "Nonexistent")
if err == nil {
t.Fatal("expected error")
}
}
func TestResolveAlbumIDListAlbumsFails(t *testing.T) {
b := &mockBridge{
albumsErr: fmt.Errorf("no access"),
assetsByAlbum: map[string][]photos.Asset{},
}
_, err := resolveAlbumID(b, "DnD")
if err == nil {
t.Fatal("expected error")
}
}
func TestCmdPhotosResolvesAlbumName(t *testing.T) {
b := &mockBridge{
albums: []photos.Album{
{ID: "ABC/L0/001", Title: "DnD"},
},
assetsByAlbum: map[string][]photos.Asset{
"ABC/L0/001": {{ID: "a1", Filename: "img.jpg", Cloud: "local"}},
},
}
out, stderr, rc := runWith([]string{"photos", "--album-id", "DnD"}, b)
if rc != 0 {
t.Fatalf("rc = %d, stderr = %q", rc, stderr)
}
if !strings.Contains(out, "a1\timg.jpg\tlocal") {
t.Errorf("out = %q", out)
}
}
func TestCmdExportResolvesAlbumName(t *testing.T) {
b := &mockBridge{
albums: []photos.Album{
{ID: "ABC/L0/001", Title: "DnD"},
},
assetsByAlbum: map[string][]photos.Asset{
"ABC/L0/001": {{ID: "a1", Filename: "img.jpg"}, {ID: "a2", Filename: "img2.jpg"}, {ID: "a3", Filename: "img3.jpg"}, {ID: "a4", Filename: "img4.jpg"}, {ID: "a5", Filename: "img5.jpg"}},
},
}
_, stderr, rc := runWith([]string{"export", "--album-id", "DnD", "--out", "/tmp/out"}, b)
if rc != 0 {
t.Fatalf("rc = %d, stderr = %q", rc, stderr)
}
if !strings.Contains(stderr, "exported 5 photos") {
t.Errorf("stderr = %q", stderr)
}
}
func TestCmdExportOriginalsResolvesAlbumName(t *testing.T) {
b := &mockBridge{
albums: []photos.Album{
{ID: "ABC/L0/001", Title: "DnD"},
},
assetsByAlbum: map[string][]photos.Asset{
"ABC/L0/001": {{ID: "a1", Filename: "img.jpg"}, {ID: "a2", Filename: "img2.jpg"}, {ID: "a3", Filename: "img3.jpg"}},
},
}
_, stderr, rc := runWith([]string{"export", "--album-id", "DnD", "--out", "/tmp/out", "--originals"}, b)
if rc != 0 {
t.Fatalf("rc = %d, stderr = %q", rc, stderr)
}
if !strings.Contains(stderr, "exported 3 original files") {
t.Errorf("stderr = %q", stderr)
}
}
func TestCmdExportPartialFailure(t *testing.T) {
call := 0
b := &mockBridge{
albums: []photos.Album{{ID: "a1", Title: "Album"}},
assetsByAlbum: map[string][]photos.Asset{
"a1": {{ID: "x1", Filename: "ok.jpg", Cloud: "local"}, {ID: "x2", Filename: "bad.jpg", Cloud: "local"}, {ID: "x3", Filename: "ok2.jpg", Cloud: "local"}},
},
exportPreviewFn: func(string, string, int, int) (photos.ExportResult, error) {
call++
if call == 2 {
return photos.ExportResult{}, fmt.Errorf("disk full")
}
return photos.ExportResult{Filename: "ok.jpg", Size: 1024, Cloud: "local"}, nil
},
}
_, stderr, rc := runWith([]string{"export", "--album-id", "Album", "--out", "/tmp"}, b)
if rc != 0 {
t.Errorf("rc = %d, stderr = %q", rc, stderr)
}
if !strings.Contains(stderr, "failed: bad.jpg: disk full") {
t.Errorf("stderr should contain failure detail, got: %q", stderr)
}
if !strings.Contains(stderr, "(1 failed)") {
t.Errorf("stderr should contain failed count, got: %q", stderr)
}
if !strings.Contains(stderr, "exported 2 photos") {
t.Errorf("stderr should contain export count, got: %q", stderr)
}
}
func TestCmdBackupAllSkippedAlbum(t *testing.T) {
b := &mockBridge{
tree: []photos.CollectionNode{{ID: "bad-album", Name: "Broken", Kind: "album"}},
assetsByAlbum: map[string][]photos.Asset{},
}
_, stderr, rc := runWith([]string{"backup-all", "--out", "/backup"}, b)
if rc != 0 {
t.Fatalf("rc = %d, stderr = %q", rc, stderr)
}
if !strings.Contains(stderr, "skipped album Broken") {
t.Errorf("stderr should contain skipped album, got: %q", stderr)
}
}
func TestResolveAlbumIDNotFoundMessage(t *testing.T) {
b := &mockBridge{
albums: []photos.Album{{ID: "x", Title: "Other"}},
assetsByAlbum: map[string][]photos.Asset{},
}
_, err := resolveAlbumID(b, "Nonexistent")
if err == nil {
t.Fatal("expected error")
}
if !strings.Contains(err.Error(), "album not found: Nonexistent") {
t.Errorf("err = %q", err.Error())
}
}