summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-30 10:42:32 +0300
committerPaul Buetow <paul@buetow.org>2026-04-30 10:42:32 +0300
commit0d7063e5c91efeb219a4be5b93525b0837c8d3be (patch)
treeaba069224950e8e0d00fadd82dc4b00e4a7c4117 /internal
parente971128cdc64aa81bba134faa65dc53bc2680d60 (diff)
fix(ca): translate sql.ErrNoRows to nil,nil for optional lookups
Repository methods that perform single-row queries now return (nil,nil) instead of leaking sql.ErrNoRows when a row is missing. This aligns with service-layer expectations (e.g. GetMediaDetail, ValidateSession, UpdateProgress, AssignTag, access checks) so normal missing data does not break app flows. Files changed: - internal/repository/media.go, user.go, set.go, set_permission.go, tag.go, note.go, playback_progress.go, playback_accumulator.go, session.go, share.go - internal/repository/sqlite_test.go (updated assertions) - internal/repository/sqlite_no_rows_test.go (new focused repository tests) - internal/service/no_rows_test.go (new focused service tests)
Diffstat (limited to 'internal')
-rw-r--r--internal/repository/media.go3
-rw-r--r--internal/repository/note.go5
-rw-r--r--internal/repository/playback_accumulator.go5
-rw-r--r--internal/repository/playback_progress.go5
-rw-r--r--internal/repository/session.go4
-rw-r--r--internal/repository/set.go3
-rw-r--r--internal/repository/set_permission.go3
-rw-r--r--internal/repository/share.go4
-rw-r--r--internal/repository/sqlite_no_rows_test.go132
-rw-r--r--internal/repository/sqlite_test.go92
-rw-r--r--internal/repository/tag.go4
-rw-r--r--internal/repository/user.go4
-rw-r--r--internal/service/no_rows_test.go160
13 files changed, 396 insertions, 28 deletions
diff --git a/internal/repository/media.go b/internal/repository/media.go
index 95016e5..3e79d5e 100644
--- a/internal/repository/media.go
+++ b/internal/repository/media.go
@@ -39,6 +39,9 @@ func scanMedia(row sqlScanner) (*model.Media, error) {
&duration, &codec, &resolution, &bitrate, &fileSize,
&thumbnail, &m.PlayCount, &deleted, &m.CreatedAt,
)
+ if err == sql.ErrNoRows {
+ return nil, nil
+ }
if err != nil {
return nil, err
}
diff --git a/internal/repository/note.go b/internal/repository/note.go
index 0809e57..0b33212 100644
--- a/internal/repository/note.go
+++ b/internal/repository/note.go
@@ -2,6 +2,7 @@ package repository
import (
"context"
+ "database/sql"
"fmt"
"github.com/paul/kiss-media-player/internal/model"
@@ -27,7 +28,9 @@ func (s *SQLite) GetNote(ctx context.Context, mediaID, userID int64) (*model.Not
mediaID, userID,
)
var n model.Note
- if err := row.Scan(&n.ID, &n.MediaID, &n.UserID, &n.Content, &n.CreatedAt, &n.UpdatedAt); err != nil {
+ if err := row.Scan(&n.ID, &n.MediaID, &n.UserID, &n.Content, &n.CreatedAt, &n.UpdatedAt); err == sql.ErrNoRows {
+ return nil, nil
+ } else if err != nil {
return nil, err
}
return &n, nil
diff --git a/internal/repository/playback_accumulator.go b/internal/repository/playback_accumulator.go
index 6b0ac22..51b4aa8 100644
--- a/internal/repository/playback_accumulator.go
+++ b/internal/repository/playback_accumulator.go
@@ -2,6 +2,7 @@ package repository
import (
"context"
+ "database/sql"
"fmt"
"github.com/paul/kiss-media-player/internal/model"
@@ -27,7 +28,9 @@ func (s *SQLite) GetAccumulator(ctx context.Context, sessionID string, mediaID i
)
var a model.PlaybackAccumulator
var counted int
- if err := row.Scan(&a.SessionID, &a.MediaID, &a.LastPosition, &a.AccumulatedSeconds, &counted, &a.UpdatedAt); err != nil {
+ if err := row.Scan(&a.SessionID, &a.MediaID, &a.LastPosition, &a.AccumulatedSeconds, &counted, &a.UpdatedAt); err == sql.ErrNoRows {
+ return nil, nil
+ } else if err != nil {
return nil, err
}
a.Counted = counted != 0
diff --git a/internal/repository/playback_progress.go b/internal/repository/playback_progress.go
index 243c8ba..1d05dcd 100644
--- a/internal/repository/playback_progress.go
+++ b/internal/repository/playback_progress.go
@@ -2,6 +2,7 @@ package repository
import (
"context"
+ "database/sql"
"fmt"
"github.com/paul/kiss-media-player/internal/model"
@@ -26,7 +27,9 @@ func (s *SQLite) GetProgress(ctx context.Context, userID, mediaID int64) (*model
userID, mediaID,
)
var p model.PlaybackProgress
- if err := row.Scan(&p.UserID, &p.MediaID, &p.PositionSeconds, &p.UpdatedAt); err != nil {
+ if err := row.Scan(&p.UserID, &p.MediaID, &p.PositionSeconds, &p.UpdatedAt); err == sql.ErrNoRows {
+ return nil, nil
+ } else if err != nil {
return nil, err
}
return &p, nil
diff --git a/internal/repository/session.go b/internal/repository/session.go
index a1f3dca..df24ed0 100644
--- a/internal/repository/session.go
+++ b/internal/repository/session.go
@@ -26,7 +26,9 @@ func (s *SQLite) GetSessionByID(ctx context.Context, id string) (*model.Session,
row := s.db.QueryRowContext(ctx,
`SELECT id, user_id, expires_at, created_at FROM sessions WHERE id = ?`, id)
var sess model.Session
- if err := row.Scan(&sess.ID, &sess.UserID, &sess.ExpiresAt, &sess.CreatedAt); err != nil {
+ if err := row.Scan(&sess.ID, &sess.UserID, &sess.ExpiresAt, &sess.CreatedAt); err == sql.ErrNoRows {
+ return nil, nil
+ } else if err != nil {
return nil, err
}
return &sess, nil
diff --git a/internal/repository/set.go b/internal/repository/set.go
index 572340b..4f06d30 100644
--- a/internal/repository/set.go
+++ b/internal/repository/set.go
@@ -24,6 +24,9 @@ func scanSet(row sqlScanner) (*model.Set, error) {
var st model.Set
var cover sql.NullString
err := row.Scan(&st.ID, &st.Name, &st.RootPath, &cover, &st.CreatedAt)
+ if err == sql.ErrNoRows {
+ return nil, nil
+ }
if err != nil {
return nil, err
}
diff --git a/internal/repository/set_permission.go b/internal/repository/set_permission.go
index e1cda07..26db9fa 100644
--- a/internal/repository/set_permission.go
+++ b/internal/repository/set_permission.go
@@ -34,6 +34,9 @@ func (s *SQLite) RevokePermission(ctx context.Context, setID, userID int64) erro
func scanPermission(row sqlScanner) (*model.SetPermission, error) {
var p model.SetPermission
err := row.Scan(&p.SetID, &p.UserID, &p.Role, &p.CreatedAt)
+ if err == sql.ErrNoRows {
+ return nil, nil
+ }
if err != nil {
return nil, err
}
diff --git a/internal/repository/share.go b/internal/repository/share.go
index 22da2fc..a7c0b8d 100644
--- a/internal/repository/share.go
+++ b/internal/repository/share.go
@@ -24,7 +24,9 @@ func (s *SQLite) CreateShare(ctx context.Context, share *model.Share) error {
func scanShare(row sqlScanner) (*model.Share, error) {
var sh model.Share
var maxUses sql.NullInt64
- if err := row.Scan(&sh.Token, &sh.MediaID, &sh.CreatedBy, &sh.CreatedAt, &sh.ExpiresAt, &maxUses, &sh.UsedCount); err != nil {
+ if err := row.Scan(&sh.Token, &sh.MediaID, &sh.CreatedBy, &sh.CreatedAt, &sh.ExpiresAt, &maxUses, &sh.UsedCount); err == sql.ErrNoRows {
+ return nil, nil
+ } else if err != nil {
return nil, err
}
if maxUses.Valid {
diff --git a/internal/repository/sqlite_no_rows_test.go b/internal/repository/sqlite_no_rows_test.go
new file mode 100644
index 0000000..48ac8a6
--- /dev/null
+++ b/internal/repository/sqlite_no_rows_test.go
@@ -0,0 +1,132 @@
+package repository
+
+import (
+ "context"
+ "testing"
+)
+
+func TestSQLite_NoRows_ReturnsNil(t *testing.T) {
+ ctx := context.Background()
+ s := newTestStore(t)
+ defer s.Close()
+
+ t.Run("GetUserByID", func(t *testing.T) {
+ u, err := s.GetUserByID(ctx, 9999)
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+ if u != nil {
+ t.Fatalf("expected nil, got %+v", u)
+ }
+ })
+
+ t.Run("GetUserByUsername", func(t *testing.T) {
+ u, err := s.GetUserByUsername(ctx, "nobody")
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+ if u != nil {
+ t.Fatalf("expected nil, got %+v", u)
+ }
+ })
+
+ t.Run("GetSetByID", func(t *testing.T) {
+ st, err := s.GetSetByID(ctx, 9999)
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+ if st != nil {
+ t.Fatalf("expected nil, got %+v", st)
+ }
+ })
+
+ t.Run("GetMediaByID", func(t *testing.T) {
+ m, err := s.GetMediaByID(ctx, 9999)
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+ if m != nil {
+ t.Fatalf("expected nil, got %+v", m)
+ }
+ })
+
+ t.Run("GetTagByID", func(t *testing.T) {
+ tag, err := s.GetTagByID(ctx, 9999)
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+ if tag != nil {
+ t.Fatalf("expected nil, got %+v", tag)
+ }
+ })
+
+ t.Run("GetTagByName", func(t *testing.T) {
+ tag, err := s.GetTagByName(ctx, "missing")
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+ if tag != nil {
+ t.Fatalf("expected nil, got %+v", tag)
+ }
+ })
+
+ t.Run("GetPermission", func(t *testing.T) {
+ p, err := s.GetPermission(ctx, 9999, 9999)
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+ if p != nil {
+ t.Fatalf("expected nil, got %+v", p)
+ }
+ })
+
+ t.Run("GetNote", func(t *testing.T) {
+ n, err := s.GetNote(ctx, 9999, 9999)
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+ if n != nil {
+ t.Fatalf("expected nil, got %+v", n)
+ }
+ })
+
+ t.Run("GetProgress", func(t *testing.T) {
+ p, err := s.GetProgress(ctx, 9999, 9999)
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+ if p != nil {
+ t.Fatalf("expected nil, got %+v", p)
+ }
+ })
+
+ t.Run("GetAccumulator", func(t *testing.T) {
+ a, err := s.GetAccumulator(ctx, "nope", 9999)
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+ if a != nil {
+ t.Fatalf("expected nil, got %+v", a)
+ }
+ })
+
+ t.Run("GetSessionByID", func(t *testing.T) {
+ sess, err := s.GetSessionByID(ctx, "nope")
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+ if sess != nil {
+ t.Fatalf("expected nil, got %+v", sess)
+ }
+ })
+
+ t.Run("GetShareByToken", func(t *testing.T) {
+ sh, err := s.GetShareByToken(ctx, "nope")
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+ if sh != nil {
+ t.Fatalf("expected nil, got %+v", sh)
+ }
+ })
+}
diff --git a/internal/repository/sqlite_test.go b/internal/repository/sqlite_test.go
index faa4fb6..1aeb23a 100644
--- a/internal/repository/sqlite_test.go
+++ b/internal/repository/sqlite_test.go
@@ -83,8 +83,12 @@ func TestSQLite_UserRepo(t *testing.T) {
if err := s.DeleteUser(ctx, id); err != nil {
t.Fatalf("delete: %v", err)
}
- if _, err := s.GetUserByID(ctx, id); err == nil {
- t.Fatal("expected error after delete")
+ u, err := s.GetUserByID(ctx, id)
+ if err != nil {
+ t.Fatalf("expected no error after delete, got %v", err)
+ }
+ if u != nil {
+ t.Fatal("expected nil user after delete")
}
},
},
@@ -158,8 +162,12 @@ func TestSQLite_SetRepo(t *testing.T) {
if err := s.DeleteSet(ctx, id); err != nil {
t.Fatalf("delete: %v", err)
}
- if _, err := s.GetSetByID(ctx, id); err == nil {
- t.Fatal("expected error after delete")
+ st, err := s.GetSetByID(ctx, id)
+ if err != nil {
+ t.Fatalf("expected no error after delete, got %v", err)
+ }
+ if st != nil {
+ t.Fatal("expected nil set after delete")
}
},
},
@@ -252,8 +260,8 @@ func TestSQLite_MediaRepo(t *testing.T) {
t.Fatalf("soft delete: %v", err)
}
m, err := s.GetMediaByID(ctx, mid)
- if err == nil {
- t.Fatal("expected error for soft-deleted media")
+ if err != nil {
+ t.Fatalf("expected no error for soft-deleted media, got %v", err)
}
if m != nil {
t.Fatalf("expected nil media for soft-deleted record, got %+v", m)
@@ -276,8 +284,12 @@ func TestSQLite_MediaRepo(t *testing.T) {
if err := s.HardDeleteMedia(ctx, mid); err != nil {
t.Fatalf("hard delete: %v", err)
}
- if _, err := s.GetMediaByID(ctx, mid); err == nil {
- t.Fatal("expected error after hard delete")
+ m, err := s.GetMediaByID(ctx, mid)
+ if err != nil {
+ t.Fatalf("expected no error after hard delete, got %v", err)
+ }
+ if m != nil {
+ t.Fatal("expected nil media after hard delete")
}
},
},
@@ -377,8 +389,12 @@ func TestSQLite_TagRepo(t *testing.T) {
if err := s.DeleteTag(ctx, id); err != nil {
t.Fatalf("delete: %v", err)
}
- if _, err := s.GetTagByID(ctx, id); err == nil {
- t.Fatal("expected error after delete")
+ tag, err := s.GetTagByID(ctx, id)
+ if err != nil {
+ t.Fatalf("expected no error after delete, got %v", err)
+ }
+ if tag != nil {
+ t.Fatal("expected nil tag after delete")
}
},
},
@@ -533,8 +549,12 @@ func TestSQLite_SessionRepo(t *testing.T) {
if err := s.DeleteSession(ctx, "abc"); err != nil {
t.Fatalf("delete: %v", err)
}
- if _, err := s.GetSessionByID(ctx, "abc"); err == nil {
- t.Fatal("expected error after delete")
+ sess, err := s.GetSessionByID(ctx, "abc")
+ if err != nil {
+ t.Fatalf("expected no error after delete, got %v", err)
+ }
+ if sess != nil {
+ t.Fatal("expected nil session after delete")
}
},
},
@@ -548,12 +568,20 @@ func TestSQLite_SessionRepo(t *testing.T) {
if err := s.DeleteExpiredSessions(ctx, now); err != nil {
t.Fatalf("delete expired: %v", err)
}
- if _, err := s.GetSessionByID(ctx, "old"); err == nil {
+ sess, err := s.GetSessionByID(ctx, "old")
+ if err != nil {
+ t.Fatalf("expected no error for old session, got %v", err)
+ }
+ if sess != nil {
t.Fatal("expected old session gone")
}
- if _, err := s.GetSessionByID(ctx, "new"); err != nil {
+ sess, err = s.GetSessionByID(ctx, "new")
+ if err != nil {
t.Fatalf("expected new session present: %v", err)
}
+ if sess == nil {
+ t.Fatal("expected new session present")
+ }
},
},
}
@@ -603,8 +631,12 @@ func TestSQLite_ShareRepo(t *testing.T) {
if err := s.DeleteShare(ctx, "tok1"); err != nil {
t.Fatalf("delete: %v", err)
}
- if _, err := s.GetShareByToken(ctx, "tok1"); err == nil {
- t.Fatal("expected error after delete")
+ sh, err := s.GetShareByToken(ctx, "tok1")
+ if err != nil {
+ t.Fatalf("expected no error after delete, got %v", err)
+ }
+ if sh != nil {
+ t.Fatal("expected nil share after delete")
}
},
},
@@ -620,12 +652,20 @@ func TestSQLite_ShareRepo(t *testing.T) {
if err := s.DeleteExpiredShares(ctx, now); err != nil {
t.Fatalf("delete expired: %v", err)
}
- if _, err := s.GetShareByToken(ctx, "old"); err == nil {
+ sh, err := s.GetShareByToken(ctx, "old")
+ if err != nil {
+ t.Fatalf("expected no error for old share, got %v", err)
+ }
+ if sh != nil {
t.Fatal("expected old share gone")
}
- if _, err := s.GetShareByToken(ctx, "new"); err != nil {
+ sh, err = s.GetShareByToken(ctx, "new")
+ if err != nil {
t.Fatalf("expected new share present: %v", err)
}
+ if sh == nil {
+ t.Fatal("expected new share present")
+ }
},
},
}
@@ -670,8 +710,12 @@ func TestSQLite_NoteRepo(t *testing.T) {
if err := s.DeleteNote(ctx, mid, uid); err != nil {
t.Fatalf("delete: %v", err)
}
- if _, err := s.GetNote(ctx, mid, uid); err == nil {
- t.Fatal("expected error after delete")
+ note, err = s.GetNote(ctx, mid, uid)
+ if err != nil {
+ t.Fatalf("expected no error after delete, got %v", err)
+ }
+ if note != nil {
+ t.Fatal("expected nil note after delete")
}
},
},
@@ -787,8 +831,12 @@ func TestSQLite_SetPermissionRepo(t *testing.T) {
if err := s.RevokePermission(ctx, sid, uid); err != nil {
t.Fatalf("revoke: %v", err)
}
- if _, err := s.GetPermission(ctx, sid, uid); err == nil {
- t.Fatal("expected error after revoke")
+ perm, err = s.GetPermission(ctx, sid, uid)
+ if err != nil {
+ t.Fatalf("expected no error after revoke, got %v", err)
+ }
+ if perm != nil {
+ t.Fatal("expected nil permission after revoke")
}
},
},
diff --git a/internal/repository/tag.go b/internal/repository/tag.go
index 72ddddd..a1ce483 100644
--- a/internal/repository/tag.go
+++ b/internal/repository/tag.go
@@ -19,7 +19,9 @@ func (s *SQLite) CreateTag(ctx context.Context, name string) (int64, error) {
func scanTag(row sqlScanner) (*model.Tag, error) {
var t model.Tag
- if err := row.Scan(&t.ID, &t.Name); err != nil {
+ if err := row.Scan(&t.ID, &t.Name); err == sql.ErrNoRows {
+ return nil, nil
+ } else if err != nil {
return nil, err
}
return &t, nil
diff --git a/internal/repository/user.go b/internal/repository/user.go
index 7f366c2..073f61d 100644
--- a/internal/repository/user.go
+++ b/internal/repository/user.go
@@ -8,6 +8,7 @@ import (
"github.com/paul/kiss-media-player/internal/model"
)
+
// CreateUser inserts a new user and returns the generated ID.
func (s *SQLite) CreateUser(ctx context.Context, user *model.User) (int64, error) {
res, err := s.db.ExecContext(ctx,
@@ -24,6 +25,9 @@ func scanUser(row sqlScanner) (*model.User, error) {
var u model.User
var admin int
err := row.Scan(&u.ID, &u.Username, &u.PasswordHash, &admin, &u.CreatedAt)
+ if err == sql.ErrNoRows {
+ return nil, nil
+ }
if err != nil {
return nil, err
}
diff --git a/internal/service/no_rows_test.go b/internal/service/no_rows_test.go
new file mode 100644
index 0000000..0369db5
--- /dev/null
+++ b/internal/service/no_rows_test.go
@@ -0,0 +1,160 @@
+package service
+
+import (
+ "context"
+ "errors"
+ "testing"
+
+ "github.com/paul/kiss-media-player/internal/model"
+ "github.com/paul/kiss-media-player/internal/repository"
+)
+
+func TestService_NoRows_ReturnsNil(t *testing.T) {
+ ctx := context.Background()
+
+ t.Run("GetMediaDetail nil media", func(t *testing.T) {
+ store := &repository.MockStore{
+ MediaRepo: repository.MockMediaRepo{
+ GetMediaByIDFunc: func(ctx context.Context, id int64) (*model.Media, error) {
+ return nil, nil
+ },
+ },
+ }
+ svc := NewMediaService(store, newMockClock(), "/tmp/media")
+ detail, err := svc.GetMediaDetail(ctx, 99, 1)
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+ if detail != nil {
+ t.Fatalf("expected nil detail, got %+v", detail)
+ }
+ })
+
+ t.Run("GetNote nil", func(t *testing.T) {
+ store := &repository.MockStore{
+ NoteRepo: repository.MockNoteRepo{
+ GetNoteFunc: func(ctx context.Context, mediaID, userID int64) (*model.Note, error) {
+ return nil, nil
+ },
+ },
+ }
+ svc := NewMediaService(store, newMockClock(), "/tmp/media")
+ note, err := svc.GetNote(ctx, 1, 1)
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+ if note != nil {
+ t.Fatalf("expected nil note, got %+v", note)
+ }
+ })
+
+ t.Run("AssignTag creates missing tag", func(t *testing.T) {
+ store := &repository.MockStore{
+ TagRepo: repository.MockTagRepo{
+ GetTagByNameFunc: func(ctx context.Context, name string) (*model.Tag, error) {
+ return nil, nil
+ },
+ CreateTagFunc: func(ctx context.Context, name string) (int64, error) {
+ return 42, nil
+ },
+ AssignTagFunc: func(ctx context.Context, mediaID, tagID int64) error {
+ return nil
+ },
+ },
+ }
+ svc := NewMediaService(store, newMockClock(), "/tmp/media")
+ if err := svc.AssignTag(ctx, 1, 1, "newtag"); err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+ })
+
+ t.Run("UpdateProgress with nil accumulator", func(t *testing.T) {
+ store := &repository.MockStore{
+ PlaybackProgressRepo: repository.MockPlaybackProgressRepo{
+ UpsertProgressFunc: func(ctx context.Context, progress *model.PlaybackProgress) error {
+ return nil
+ },
+ },
+ PlaybackAccumulatorRepo: repository.MockPlaybackAccumulatorRepo{
+ GetAccumulatorFunc: func(ctx context.Context, sessionID string, mediaID int64) (*model.PlaybackAccumulator, error) {
+ return nil, nil
+ },
+ UpsertAccumulatorFunc: func(ctx context.Context, acc *model.PlaybackAccumulator) error {
+ return nil
+ },
+ },
+ MediaRepo: repository.MockMediaRepo{
+ IncrementPlayCountFunc: func(ctx context.Context, id int64) error {
+ return nil
+ },
+ },
+ }
+ svc := NewProgressService(store, newMockClock())
+ if err := svc.UpdateProgress(ctx, "sess", 1, 10, 5); err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+ })
+
+ t.Run("ValidateShareToken nil", func(t *testing.T) {
+ store := &repository.MockStore{
+ ShareRepo: repository.MockShareRepo{
+ GetShareByTokenFunc: func(ctx context.Context, token string) (*model.Share, error) {
+ return nil, nil
+ },
+ },
+ }
+ svc := NewMediaService(store, newMockClock(), "/tmp/media")
+ sh, err := svc.ValidateShareToken(ctx, "nope")
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+ if sh != nil {
+ t.Fatalf("expected nil share, got %+v", sh)
+ }
+ })
+
+ t.Run("StreamSharedMedia missing share", func(t *testing.T) {
+ store := &repository.MockStore{
+ ShareRepo: repository.MockShareRepo{
+ GetShareByTokenFunc: func(ctx context.Context, token string) (*model.Share, error) {
+ return nil, nil
+ },
+ },
+ }
+ svc := NewMediaService(store, newMockClock(), "/tmp/media")
+ _, err := svc.StreamSharedMedia(ctx, "nope")
+ if err == nil {
+ t.Fatal("expected error for missing share")
+ }
+ })
+
+ t.Run("verifyAccess missing permission", func(t *testing.T) {
+ store := &repository.MockStore{
+ MediaRepo: repository.MockMediaRepo{
+ GetMediaByIDFunc: func(ctx context.Context, id int64) (*model.Media, error) {
+ return &model.Media{ID: 1, SetID: 1}, nil
+ },
+ },
+ UserRepo: repository.MockUserRepo{
+ GetUserByIDFunc: func(ctx context.Context, id int64) (*model.User, error) {
+ return &model.User{ID: 1, IsAdmin: false}, nil
+ },
+ },
+ SetRepo: repository.MockSetRepo{
+ GetSetByIDFunc: func(ctx context.Context, id int64) (*model.Set, error) {
+ return &model.Set{ID: 1}, nil
+ },
+ },
+ SetPermissionRepo: repository.MockSetPermissionRepo{
+ GetPermissionFunc: func(ctx context.Context, setID, userID int64) (*model.SetPermission, error) {
+ return nil, nil
+ },
+ },
+ }
+ svc := NewMediaService(store, newMockClock(), "/tmp/media")
+ _, err := svc.StreamMedia(ctx, 1, 1)
+ if !errors.Is(err, ErrForbidden) {
+ t.Fatalf("expected ErrForbidden, got %v", err)
+ }
+ })
+}