diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-30 10:42:32 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-30 10:42:32 +0300 |
| commit | 0d7063e5c91efeb219a4be5b93525b0837c8d3be (patch) | |
| tree | aba069224950e8e0d00fadd82dc4b00e4a7c4117 | |
| parent | e971128cdc64aa81bba134faa65dc53bc2680d60 (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)
| -rw-r--r-- | internal/repository/media.go | 3 | ||||
| -rw-r--r-- | internal/repository/note.go | 5 | ||||
| -rw-r--r-- | internal/repository/playback_accumulator.go | 5 | ||||
| -rw-r--r-- | internal/repository/playback_progress.go | 5 | ||||
| -rw-r--r-- | internal/repository/session.go | 4 | ||||
| -rw-r--r-- | internal/repository/set.go | 3 | ||||
| -rw-r--r-- | internal/repository/set_permission.go | 3 | ||||
| -rw-r--r-- | internal/repository/share.go | 4 | ||||
| -rw-r--r-- | internal/repository/sqlite_no_rows_test.go | 132 | ||||
| -rw-r--r-- | internal/repository/sqlite_test.go | 92 | ||||
| -rw-r--r-- | internal/repository/tag.go | 4 | ||||
| -rw-r--r-- | internal/repository/user.go | 4 | ||||
| -rw-r--r-- | internal/service/no_rows_test.go | 160 |
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) + } + }) +} |
