mirror of
https://github.com/ThisTine/Snip.git
synced 2026-08-18 23:18:47 +07:00
159 lines
4.5 KiB
Go
159 lines
4.5 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/snip/backend/internal/adapter/memory"
|
|
"github.com/snip/backend/internal/domain"
|
|
)
|
|
|
|
// --- test doubles ---
|
|
|
|
type fakeHasher struct{}
|
|
|
|
func (fakeHasher) Hash(p string) (string, error) { return "h:" + p, nil }
|
|
func (fakeHasher) Compare(hash, p string) bool { return hash == "h:"+p }
|
|
|
|
type countingRecorder struct {
|
|
mu sync.Mutex
|
|
n map[string]int
|
|
}
|
|
|
|
func newCountingRecorder() *countingRecorder { return &countingRecorder{n: map[string]int{}} }
|
|
func (r *countingRecorder) Record(code string) {
|
|
r.mu.Lock()
|
|
r.n[code]++
|
|
r.mu.Unlock()
|
|
}
|
|
func (r *countingRecorder) count(code string) int {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
return r.n[code]
|
|
}
|
|
|
|
func newLinkSvc() *LinkService {
|
|
return NewLinkService(memory.NewLinkRepo(), memory.NewCache(), fakeHasher{})
|
|
}
|
|
|
|
// --- tests ---
|
|
|
|
func TestCreateRandomAndMemorable(t *testing.T) {
|
|
svc := newLinkSvc()
|
|
ctx := context.Background()
|
|
|
|
r, err := svc.Create(ctx, CreateInput{LongURL: "acme.com/a", Mode: domain.ModeRandom}, "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if r.Code == "" || r.LongURL != "https://acme.com/a" {
|
|
t.Fatalf("bad random link: %+v", r)
|
|
}
|
|
|
|
m, err := svc.Create(ctx, CreateInput{LongURL: "https://acme.com/b", Mode: domain.ModeMemorable}, "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(m.Code) < 5 {
|
|
t.Fatalf("memorable code too short: %s", m.Code)
|
|
}
|
|
}
|
|
|
|
func TestCustomRequiresAuthAndIsUnique(t *testing.T) {
|
|
svc := newLinkSvc()
|
|
ctx := context.Background()
|
|
|
|
if _, err := svc.Create(ctx, CreateInput{LongURL: "acme.com", Mode: domain.ModeCustom, CustomAlias: "promo"}, ""); !errors.Is(err, domain.ErrUnauthorized) {
|
|
t.Fatalf("anon custom should be unauthorized, got %v", err)
|
|
}
|
|
|
|
if _, err := svc.Create(ctx, CreateInput{LongURL: "acme.com", Mode: domain.ModeCustom, CustomAlias: "promo"}, "u1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := svc.Create(ctx, CreateInput{LongURL: "acme.com", Mode: domain.ModeCustom, CustomAlias: "promo"}, "u1"); !errors.Is(err, domain.ErrCodeTaken) {
|
|
t.Fatalf("duplicate alias should conflict, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCustomRejectsReservedPath(t *testing.T) {
|
|
svc := newLinkSvc()
|
|
if _, err := svc.Create(context.Background(), CreateInput{LongURL: "acme.com", Mode: domain.ModeCustom, CustomAlias: "dashboard"}, "u1"); !errors.Is(err, domain.ErrReserved) {
|
|
t.Fatalf("reserved alias should be rejected, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestInvalidURLRejected(t *testing.T) {
|
|
svc := newLinkSvc()
|
|
if _, err := svc.Create(context.Background(), CreateInput{LongURL: "not a url", Mode: domain.ModeRandom}, ""); !errors.Is(err, domain.ErrInvalidURL) {
|
|
t.Fatalf("expected ErrInvalidURL, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPinLifecycleAndOwnership(t *testing.T) {
|
|
svc := newLinkSvc()
|
|
ctx := context.Background()
|
|
|
|
link, err := svc.Create(ctx, CreateInput{LongURL: "acme.com", Mode: domain.ModeRandom, Pin: "123456"}, "owner")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !link.HasPin {
|
|
t.Fatal("expected pin set")
|
|
}
|
|
|
|
// A different owner cannot touch it.
|
|
if _, err := svc.UpdateDestination(ctx, link.ID, "intruder", "https://evil.com"); !errors.Is(err, domain.ErrForbidden) {
|
|
t.Fatalf("expected forbidden, got %v", err)
|
|
}
|
|
|
|
// Owner removes the pin.
|
|
updated, err := svc.SetPin(ctx, link.ID, "owner", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if updated.HasPin {
|
|
t.Fatal("pin should be removed")
|
|
}
|
|
|
|
if err := svc.Delete(ctx, link.ID, "owner"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := svc.List(ctx, "owner", "", 1, 10); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestListPaginationAndSearch(t *testing.T) {
|
|
svc := newLinkSvc()
|
|
ctx := context.Background()
|
|
for _, alias := range []string{"alpha", "beta", "gamma", "delta", "epsilon"} {
|
|
if _, err := svc.Create(ctx, CreateInput{LongURL: "acme.com/" + alias, Mode: domain.ModeCustom, CustomAlias: alias}, "owner"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// page 1 of size 2 → 2 items, total 5
|
|
p, err := svc.List(ctx, "owner", "", 1, 2)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if p.Total != 5 || len(p.Items) != 2 {
|
|
t.Fatalf("want total=5 items=2, got total=%d items=%d", p.Total, len(p.Items))
|
|
}
|
|
// page 3 → 1 item
|
|
if p, _ := svc.List(ctx, "owner", "", 3, 2); len(p.Items) != 1 {
|
|
t.Fatalf("page 3 should have 1 item, got %d", len(p.Items))
|
|
}
|
|
// search narrows
|
|
if p, _ := svc.List(ctx, "owner", "alpha", 1, 10); p.Total != 1 || p.Items[0].Code != "alpha" {
|
|
t.Fatalf("search 'alpha' should match 1, got total=%d", p.Total)
|
|
}
|
|
// stats reflect all links
|
|
s, _ := svc.Stats(ctx, "owner")
|
|
if s.TotalLinks != 5 {
|
|
t.Fatalf("stats total links want 5, got %d", s.TotalLinks)
|
|
}
|
|
}
|