From 54bc9013aa0463962a1cfd67fa278de3d401d85a Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Thu, 30 Apr 2026 12:45:18 +0300 Subject: ja: implement public share landing page and improve error mapping - Add web/share.html with a minimal HTML5 video player, theme variables, centered layout, play overlay, and Back to Home link. - Update handleSharePage to return HTML for browser Accept headers (text/html or empty) and JSON for application/json. Return 410 Gone for expired shares. - Update handleShareStream to map service errors to distinct HTTP codes: ErrShareExpired -> 410, ErrShareNotFound/ErrMediaNotFound -> 404. - Improve ValidateShareToken and StreamSharedMedia to return sentinel errors (ErrShareNotFound, ErrShareExpired, ErrMediaNotFound) instead of generic string errors. - Update and add tests for share page HTML/JSON negotiation and share stream 404/410 responses. --- internal/service/media.go | 18 +++++++++--------- internal/service/media_test.go | 9 ++++++--- internal/service/no_rows_test.go | 8 ++++---- 3 files changed, 19 insertions(+), 16 deletions(-) (limited to 'internal/service') diff --git a/internal/service/media.go b/internal/service/media.go index f950db3..6f4b2db 100644 --- a/internal/service/media.go +++ b/internal/service/media.go @@ -35,8 +35,11 @@ func NewMediaService(store repository.MediaServiceStore, clk clock.Clock, mediaR // Sentinel errors returned by the media service layer. var ( - ErrNotFound = errors.New("not found") - ErrForbidden = errors.New("access denied") + ErrNotFound = errors.New("not found") + ErrForbidden = errors.New("access denied") + ErrShareNotFound = errors.New("share not found") + ErrShareExpired = errors.New("share expired") + ErrMediaNotFound = errors.New("media not found") ) func (s *mediaService) ListSets(ctx context.Context, userID int64) ([]model.Set, error) { @@ -510,16 +513,16 @@ func (s *mediaService) ValidateShareToken(ctx context.Context, token string) (*m return nil, fmt.Errorf("get share: %w", err) } if share == nil { - return nil, nil + return nil, ErrShareNotFound } now := s.clock.Now() if now.After(share.ExpiresAt) { - return nil, nil + return nil, ErrShareExpired } if share.MaxUses != nil && share.UsedCount >= *share.MaxUses { - return nil, nil + return nil, ErrShareExpired } return share, nil @@ -530,16 +533,13 @@ func (s *mediaService) StreamSharedMedia(ctx context.Context, token string) (*Fi if err != nil { return nil, err } - if share == nil { - return nil, errors.New("invalid or expired share") - } media, err := s.store.GetMediaByID(ctx, share.MediaID) if err != nil { return nil, fmt.Errorf("get media: %w", err) } if media == nil { - return nil, errors.New("media not found") + return nil, ErrMediaNotFound } _ = s.store.UseShare(ctx, token) diff --git a/internal/service/media_test.go b/internal/service/media_test.go index 1ab7dfb..33912b4 100644 --- a/internal/service/media_test.go +++ b/internal/service/media_test.go @@ -905,14 +905,17 @@ func TestMediaService_ValidateShareToken(t *testing.T) { } svc := NewMediaService(store, newMockClock(), "/tmp/media") res, err := svc.ValidateShareToken(ctx, "abc") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } if tt.wantValid { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } if res == nil { t.Fatal("expected valid share") } } else { + if err == nil { + t.Fatal("expected error for invalid share") + } if res != nil { t.Fatal("expected nil share") } diff --git a/internal/service/no_rows_test.go b/internal/service/no_rows_test.go index 3606b63..9200668 100644 --- a/internal/service/no_rows_test.go +++ b/internal/service/no_rows_test.go @@ -135,8 +135,8 @@ func TestService_NoRows_ReturnsNil(t *testing.T) { } svc := NewMediaService(store, newMockClock(), "/tmp/media") sh, err := svc.ValidateShareToken(ctx, "nope") - if err != nil { - t.Fatalf("expected no error, got %v", err) + if !errors.Is(err, ErrShareNotFound) { + t.Fatalf("expected ErrShareNotFound, got %v", err) } if sh != nil { t.Fatalf("expected nil share, got %+v", sh) @@ -153,8 +153,8 @@ func TestService_NoRows_ReturnsNil(t *testing.T) { } svc := NewMediaService(store, newMockClock(), "/tmp/media") _, err := svc.StreamSharedMedia(ctx, "nope") - if err == nil { - t.Fatal("expected error for missing share") + if !errors.Is(err, ErrShareNotFound) { + t.Fatalf("expected ErrShareNotFound, got %v", err) } }) -- cgit v1.2.3