package services import ( "context" "errors" "io" "net" "net/http" "net/url" "strings" "testing" ) func TestFetchTitleSuccess(t *testing.T) { svc := NewURLMetadataService() svc.client = newTestClient(t, func(r *http.Request) (*http.Response, error) { body := io.NopCloser(strings.NewReader(" Example\n Title ")) header := make(http.Header) header.Set("Content-Type", "text/html; charset=utf-8") return &http.Response{StatusCode: http.StatusOK, Body: body, Header: header}, nil }) mockResolver := NewMockDNSResolver() mockResolver.SetLookupResult("example.com", []net.IP{net.ParseIP("8.8.8.8")}) svc.resolver = mockResolver title, err := svc.FetchTitle(context.Background(), "https://example.com") if err != nil { t.Fatalf("FetchTitle returned error: %v", err) } if title != "Example Title" { t.Fatalf("expected sanitized title, got %q", title) } } func TestFetchTitleErrors(t *testing.T) { svc := NewURLMetadataService() if _, err := svc.FetchTitle(context.Background(), ""); err == nil { t.Fatal("expected error for empty URL") } if _, err := svc.FetchTitle(context.Background(), ":://invalid"); err == nil { t.Fatal("expected parse error for invalid URL") } if _, err := svc.FetchTitle(context.Background(), "ftp://example.com"); !errors.Is(err, ErrUnsupportedScheme) { t.Fatalf("expected ErrUnsupportedScheme, got %v", err) } } func TestFetchTitleHTTPFailures(t *testing.T) { tests := []struct { name string handler func(*http.Request) (*http.Response, error) wantErr string wantTarget error }{ { name: "NonOKStatus", handler: func(*http.Request) (*http.Response, error) { body := io.NopCloser(strings.NewReader("error")) return &http.Response{StatusCode: http.StatusBadGateway, Body: body, Header: make(http.Header)}, nil }, wantErr: "unexpected status code", }, { name: "NoTitle", handler: func(*http.Request) (*http.Response, error) { body := io.NopCloser(strings.NewReader("No title")) header := make(http.Header) header.Set("Content-Type", "text/html; charset=utf-8") return &http.Response{StatusCode: http.StatusOK, Body: body, Header: header}, nil }, wantTarget: ErrTitleNotFound, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { svc := NewURLMetadataService() svc.client = newTestClient(t, tc.handler) mockResolver := NewMockDNSResolver() mockResolver.SetLookupResult("example.com", []net.IP{net.ParseIP("8.8.8.8")}) svc.resolver = mockResolver _, err := svc.FetchTitle(context.Background(), "https://example.com") if err == nil { t.Fatal("expected error but got nil") } if tc.wantTarget != nil { if !errors.Is(err, tc.wantTarget) { t.Fatalf("expected error %v, got %v", tc.wantTarget, err) } return } if !strings.Contains(err.Error(), tc.wantErr) { t.Fatalf("expected error to contain %q, got %v", tc.wantErr, err) } }) } } func TestFetchTitleSkipsEmptyTitles(t *testing.T) { svc := NewURLMetadataService() sampleHTML := ` Real Video Title - YouTube` svc.client = newTestClient(t, func(r *http.Request) (*http.Response, error) { headers := make(http.Header) headers.Set("Content-Type", "text/html; charset=utf-8") return &http.Response{ StatusCode: http.StatusOK, Header: headers, Body: io.NopCloser(strings.NewReader(sampleHTML)), }, nil }) mockResolver := NewMockDNSResolver() mockResolver.SetLookupResult("www.youtube.com", []net.IP{net.ParseIP("8.8.8.8")}) svc.resolver = mockResolver title, err := svc.FetchTitle(context.Background(), "https://www.youtube.com/watch?v=dQw4w9WgXcQ") if err != nil { t.Fatalf("FetchTitle returned error: %v", err) } if title != "Real Video Title - YouTube" { t.Fatalf("expected real title, got %q", title) } } type roundTripFunc func(*http.Request) (*http.Response, error) func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { return f(req) } func newTestClient(t *testing.T, fn roundTripFunc) *http.Client { t.Helper() return &http.Client{Transport: fn} } type MockDNSResolver struct { lookupResults map[string][]net.IP lookupErrors map[string]error } func NewMockDNSResolver() *MockDNSResolver { return &MockDNSResolver{ lookupResults: make(map[string][]net.IP), lookupErrors: make(map[string]error), } } func (m *MockDNSResolver) LookupIP(hostname string) ([]net.IP, error) { if err, exists := m.lookupErrors[hostname]; exists { return nil, err } if ips, exists := m.lookupResults[hostname]; exists { return ips, nil } if ip := net.ParseIP(hostname); ip != nil { return []net.IP{ip}, nil } return []net.IP{net.ParseIP("8.8.8.8")}, nil } func (m *MockDNSResolver) SetLookupResult(hostname string, ips []net.IP) { m.lookupResults[hostname] = ips } func (m *MockDNSResolver) SetLookupError(hostname string, err error) { m.lookupErrors[hostname] = err } func TestSSRFProtection(t *testing.T) { tests := []struct { name string url string expectError bool errorType error }{ { name: "localhost blocked", url: "http://localhost:8080", expectError: true, errorType: ErrSSRFBlocked, }, { name: "127.0.0.1 blocked", url: "http://127.0.0.1:8080", expectError: true, errorType: ErrSSRFBlocked, }, { name: "private IP 10.0.0.1 blocked", url: "http://10.0.0.1:8080", expectError: true, errorType: ErrSSRFBlocked, }, { name: "private IP 192.168.1.1 blocked", url: "http://192.168.1.1:8080", expectError: true, errorType: ErrSSRFBlocked, }, { name: "private IP 172.16.0.1 blocked", url: "http://172.16.0.1:8080", expectError: true, errorType: ErrSSRFBlocked, }, { name: "link-local 169.254.0.1 blocked", url: "http://169.254.0.1:8080", expectError: true, errorType: ErrSSRFBlocked, }, { name: "multicast 224.0.0.1 blocked", url: "http://224.0.0.1:8080", expectError: true, errorType: ErrSSRFBlocked, }, { name: "valid public domain allowed", url: "https://example.com", expectError: false, }, { name: "IPv6 localhost blocked", url: "http://[::1]:8080", expectError: true, errorType: ErrSSRFBlocked, }, { name: "empty host blocked", url: "http://", expectError: true, errorType: ErrSSRFBlocked, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { svc := NewURLMetadataService() mockResolver := NewMockDNSResolver() svc.resolver = mockResolver if tt.url == "https://example.com" { mockResolver.SetLookupResult("example.com", []net.IP{net.ParseIP("8.8.8.8")}) } if tt.expectError && strings.Contains(tt.url, "://") { if u, err := url.Parse(tt.url); err == nil { hostname := u.Hostname() if hostname != "" { if ip := net.ParseIP(hostname); ip != nil { mockResolver.SetLookupResult(hostname, []net.IP{ip}) } } } } if !tt.expectError && tt.url == "https://example.com" { svc.client = newTestClient(t, func(r *http.Request) (*http.Response, error) { body := io.NopCloser(strings.NewReader("Test Title")) header := make(http.Header) header.Set("Content-Type", "text/html; charset=utf-8") return &http.Response{StatusCode: http.StatusOK, Body: body, Header: header}, nil }) } _, err := svc.FetchTitle(context.Background(), tt.url) if tt.expectError { if err == nil { t.Fatalf("expected error for URL %q, got nil", tt.url) } if tt.errorType != nil && !errors.Is(err, tt.errorType) { t.Fatalf("expected error type %v, got %v", tt.errorType, err) } } else { if err != nil { t.Fatalf("unexpected error for URL %q: %v", tt.url, err) } } }) } } func TestRedirectLimiting(t *testing.T) { svc := NewURLMetadataService() mockResolver := NewMockDNSResolver() mockResolver.SetLookupResult("example.com", []net.IP{net.ParseIP("8.8.8.8")}) svc.resolver = mockResolver svc.client = &http.Client{ Timeout: requestTimeout, CheckRedirect: func(req *http.Request, via []*http.Request) error { if len(via) >= maxRedirects { return ErrTooManyRedirects } return nil }, Transport: newTestClient(t, func(r *http.Request) (*http.Response, error) { return &http.Response{ StatusCode: http.StatusMovedPermanently, Header: http.Header{"Location": []string{"https://example.com/redirect"}}, Body: io.NopCloser(strings.NewReader("")), }, nil }).Transport, } _, err := svc.FetchTitle(context.Background(), "https://example.com") if err == nil { t.Fatal("expected error for too many redirects") } if !errors.Is(err, ErrTooManyRedirects) { t.Fatalf("expected ErrTooManyRedirects, got %v", err) } } func TestValidateURLForSSRF(t *testing.T) { tests := []struct { name string url string expectError bool }{ { name: "valid public URL", url: "https://example.com", expectError: false, }, { name: "localhost blocked", url: "http://localhost", expectError: true, }, { name: "127.0.0.1 blocked", url: "http://127.0.0.1", expectError: true, }, { name: "private IP blocked", url: "http://192.168.1.1", expectError: true, }, { name: "empty host blocked", url: "http://", expectError: true, }, { name: "nil URL blocked", url: "", expectError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var u *url.URL var err error if tt.url != "" { u, err = url.Parse(tt.url) if err != nil { t.Fatalf("failed to parse URL %q: %v", tt.url, err) } } svc := NewURLMetadataService() mockResolver := NewMockDNSResolver() svc.resolver = mockResolver if tt.url == "https://example.com" { mockResolver.SetLookupResult("example.com", []net.IP{net.ParseIP("8.8.8.8")}) } if u != nil && u.Hostname() != "" { if ip := net.ParseIP(u.Hostname()); ip != nil { mockResolver.SetLookupResult(u.Hostname(), []net.IP{ip}) } } err = svc.validateURLForSSRF(u) if tt.expectError { if err == nil { t.Fatalf("expected error for URL %q, got nil", tt.url) } if !errors.Is(err, ErrSSRFBlocked) { t.Fatalf("expected ErrSSRFBlocked, got %v", err) } } else { if err != nil && !strings.Contains(err.Error(), "fetch url") { t.Fatalf("unexpected error for URL %q: %v", tt.url, err) } } }) } } func TestIsPrivateOrReservedIP(t *testing.T) { tests := []struct { name string ip string expected bool }{ {"10.0.0.1", "10.0.0.1", true}, {"10.255.255.255", "10.255.255.255", true}, {"172.16.0.1", "172.16.0.1", true}, {"172.31.255.255", "172.31.255.255", true}, {"192.168.1.1", "192.168.1.1", true}, {"192.168.255.255", "192.168.255.255", true}, {"127.0.0.1", "127.0.0.1", true}, {"169.254.0.1", "169.254.0.1", true}, {"224.0.0.1", "224.0.0.1", true}, {"240.0.0.1", "240.0.0.1", true}, {"8.8.8.8", "8.8.8.8", false}, {"1.1.1.1", "1.1.1.1", false}, {"74.125.224.72", "74.125.224.72", false}, {"nil IP", "", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var ip net.IP if tt.ip != "" { ip = net.ParseIP(tt.ip) } result := isPrivateOrReservedIP(ip) if result != tt.expected { t.Fatalf("expected %v for IP %q, got %v", tt.expected, tt.ip, result) } }) } } func TestIsLocalhost(t *testing.T) { tests := []struct { hostname string expected bool }{ {"localhost", true}, {"LOCALHOST", true}, {"127.0.0.1", true}, {"::1", true}, {"0.0.0.0", true}, {"0:0:0:0:0:0:0:1", true}, {"0:0:0:0:0:0:0:0", true}, {"example.com", false}, {"192.168.1.1", false}, {"8.8.8.8", false}, {"", false}, } for _, tt := range tests { t.Run(tt.hostname, func(t *testing.T) { result := isLocalhost(tt.hostname) if result != tt.expected { t.Fatalf("expected %v for hostname %q, got %v", tt.expected, tt.hostname, result) } }) } } func TestExtractFromTitleTag(t *testing.T) { svc := NewURLMetadataService() tests := []struct { name string html string expected string }{ { name: "simple title", html: `Test Title`, expected: "Test Title", }, { name: "title with whitespace", html: ` Test Title `, expected: "Test Title", }, { name: "title with newlines", html: `Test` + "\n" + `Title`, expected: "Test Title", }, { name: "empty title", html: ``, expected: "", }, { name: "whitespace only title", html: ` `, expected: "", }, { name: "no title tag", html: ``, expected: "", }, { name: "title in svg (first title found)", html: `SVG TitleReal Title`, expected: "SVG Title", }, { name: "multiple title tags (first non-empty)", html: `First TitleSecond Title`, expected: "First Title", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := svc.ExtractFromTitleTag(tt.html) if result != tt.expected { t.Fatalf("expected %q, got %q", tt.expected, result) } }) } } func TestExtractFromOpenGraph(t *testing.T) { svc := NewURLMetadataService() tests := []struct { name string html string expected string }{ { name: "simple og:title", html: ``, expected: "Open Graph Title", }, { name: "og:title with whitespace", html: ``, expected: "Open Graph Title", }, { name: "empty og:title", html: ``, expected: "", }, { name: "whitespace only og:title", html: ``, expected: "", }, { name: "no og:title", html: ``, expected: "", }, { name: "case insensitive property", html: ``, expected: "Case Insensitive Title", }, { name: "multiple og:title (first one)", html: ``, expected: "First Title", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := svc.ExtractFromOpenGraph(tt.html) if result != tt.expected { t.Fatalf("expected %q, got %q", tt.expected, result) } }) } } func TestExtractFromJSONLD(t *testing.T) { svc := NewURLMetadataService() tests := []struct { name string html string expected string }{ { name: "VideoObject with name", html: `{"@type":"VideoObject","name":"Video Title"}`, expected: "Video Title", }, { name: "WebPage with name", html: `{"@type":"WebPage","name":"Page Title"}`, expected: "Page Title", }, { name: "VideoObject with whitespace in name", html: `{"@type":"VideoObject","name":" Video Title "}`, expected: "Video Title", }, { name: "empty name", html: `{"@type":"VideoObject","name":""}`, expected: "", }, { name: "whitespace only name", html: `{"@type":"VideoObject","name":" "}`, expected: "", }, { name: "no name field", html: `{"@type":"VideoObject","description":"Description"}`, expected: "", }, { name: "wrong type", html: `{"@type":"Article","name":"Article Title"}`, expected: "", }, { name: "no @type", html: `{"name":"Some Title"}`, expected: "", }, { name: "multiple objects (first VideoObject)", html: `{"@type":"VideoObject","name":"Video Title"} {"@type":"WebPage","name":"Page Title"}`, expected: "Video Title", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := svc.ExtractFromJSONLD(tt.html) if result != tt.expected { t.Fatalf("expected %q, got %q", tt.expected, result) } }) } } func TestExtractFromTwitterCard(t *testing.T) { svc := NewURLMetadataService() tests := []struct { name string html string expected string }{ { name: "simple twitter:title", html: ``, expected: "Twitter Title", }, { name: "twitter:title with whitespace", html: ``, expected: "Twitter Title", }, { name: "empty twitter:title", html: ``, expected: "", }, { name: "whitespace only twitter:title", html: ``, expected: "", }, { name: "no twitter:title", html: ``, expected: "", }, { name: "case insensitive name", html: ``, expected: "Case Insensitive Title", }, { name: "multiple twitter:title (first one)", html: ``, expected: "First Title", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := svc.ExtractFromTwitterCard(tt.html) if result != tt.expected { t.Fatalf("expected %q, got %q", tt.expected, result) } }) } } func TestExtractFromMetaTags(t *testing.T) { svc := NewURLMetadataService() tests := []struct { name string html string expected string }{ { name: "simple meta title", html: ``, expected: "Meta Title", }, { name: "meta title with whitespace", html: ``, expected: "Meta Title", }, { name: "empty meta title", html: ``, expected: "", }, { name: "whitespace only meta title", html: ``, expected: "", }, { name: "no meta title", html: ``, expected: "", }, { name: "case insensitive name", html: ``, expected: "Case Insensitive Title", }, { name: "multiple meta title (first one)", html: ``, expected: "First Title", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := svc.extractFromMetaTags(tt.html) if result != tt.expected { t.Fatalf("expected %q, got %q", tt.expected, result) } }) } } func TestExtractTitleFromHTML(t *testing.T) { svc := NewURLMetadataService() tests := []struct { name string html string expected string }{ { name: "title tag takes precedence", html: `Title Tag`, expected: "Title Tag", }, { name: "og:title fallback when no title tag", html: ``, expected: "OG Title", }, { name: "JSON-LD fallback when no title or og", html: ``, expected: "JSON Title", }, { name: "twitter fallback when no title, og, or json", html: ``, expected: "Twitter Title", }, { name: "meta title fallback when no other methods work", html: ``, expected: "Meta Title", }, { name: "empty title tag falls back to og:title", html: ``, expected: "OG Title", }, { name: "whitespace title tag falls back to og:title", html: ` `, expected: "OG Title", }, { name: "no title found", html: ``, expected: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := svc.ExtractTitleFromHTML(tt.html) if result != tt.expected { t.Fatalf("expected %q, got %q", tt.expected, result) } }) } } func TestOptimizedTitleClean(t *testing.T) { svc := NewURLMetadataService() tests := []struct { name string input string expected string }{ { name: "simple title", input: "Simple Title", expected: "Simple Title", }, { name: "leading and trailing whitespace", input: " Title ", expected: "Title", }, { name: "multiple spaces", input: "Title with spaces", expected: "Title with spaces", }, { name: "tabs and newlines", input: "Title\twith\nnewlines\r\nand\ttabs", expected: "Title with newlines and tabs", }, { name: "mixed whitespace", input: " \t Title \n with \r\n mixed \t whitespace ", expected: "Title with mixed whitespace", }, { name: "empty string", input: "", expected: "", }, { name: "whitespace only", input: " \t\n\r ", expected: "", }, { name: "single character", input: "A", expected: "A", }, { name: "single character with whitespace", input: " A ", expected: "A", }, { name: "unicode characters", input: " Title with émojis 🎉 ", expected: "Title with émojis 🎉", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := svc.optimizedTitleClean(tt.input) if result != tt.expected { t.Fatalf("expected %q, got %q", tt.expected, result) } }) } } func TestContentTypeValidation(t *testing.T) { svc := NewURLMetadataService() tests := []struct { name string contentType string expectError bool }{ { name: "valid HTML content type", contentType: "text/html; charset=utf-8", expectError: false, }, { name: "HTML without charset", contentType: "text/html", expectError: false, }, { name: "HTML with different charset", contentType: "text/html; charset=iso-8859-1", expectError: false, }, { name: "XHTML content type", contentType: "application/xhtml+xml", expectError: true, }, { name: "invalid content type - JSON", contentType: "application/json", expectError: true, }, { name: "invalid content type - plain text", contentType: "text/plain", expectError: true, }, { name: "invalid content type - XML", contentType: "application/xml", expectError: true, }, { name: "empty content type", contentType: "", expectError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { svc.client = newTestClient(t, func(r *http.Request) (*http.Response, error) { body := io.NopCloser(strings.NewReader("Test Title")) header := make(http.Header) header.Set("Content-Type", tt.contentType) return &http.Response{StatusCode: http.StatusOK, Body: body, Header: header}, nil }) mockResolver := NewMockDNSResolver() mockResolver.SetLookupResult("example.com", []net.IP{net.ParseIP("8.8.8.8")}) svc.resolver = mockResolver _, err := svc.FetchTitle(context.Background(), "https://example.com") if tt.expectError { if err == nil { t.Fatal("expected error but got nil") } if !errors.Is(err, ErrTitleNotFound) { t.Fatalf("expected ErrTitleNotFound, got %v", err) } } else { if err != nil { t.Fatalf("unexpected error: %v", err) } } }) } } func TestContentLengthLimit(t *testing.T) { svc := NewURLMetadataService() svc.client = newTestClient(t, func(r *http.Request) (*http.Response, error) { body := io.NopCloser(strings.NewReader("Test Title")) header := make(http.Header) header.Set("Content-Type", "text/html; charset=utf-8") return &http.Response{ StatusCode: http.StatusOK, Body: body, Header: header, ContentLength: 15000000, }, nil }) mockResolver := NewMockDNSResolver() mockResolver.SetLookupResult("example.com", []net.IP{net.ParseIP("8.8.8.8")}) svc.resolver = mockResolver _, err := svc.FetchTitle(context.Background(), "https://example.com") if err == nil { t.Fatal("expected error for content length exceeding limit") } if !errors.Is(err, ErrTitleNotFound) { t.Fatalf("expected ErrTitleNotFound, got %v", err) } } func TestHTTPHeaders(t *testing.T) { svc := NewURLMetadataService() var capturedRequest *http.Request svc.client = newTestClient(t, func(r *http.Request) (*http.Response, error) { capturedRequest = r body := io.NopCloser(strings.NewReader("Test Title")) header := make(http.Header) header.Set("Content-Type", "text/html; charset=utf-8") return &http.Response{StatusCode: http.StatusOK, Body: body, Header: header}, nil }) mockResolver := NewMockDNSResolver() mockResolver.SetLookupResult("example.com", []net.IP{net.ParseIP("8.8.8.8")}) svc.resolver = mockResolver _, err := svc.FetchTitle(context.Background(), "https://example.com") if err != nil { t.Fatalf("unexpected error: %v", err) } expectedUserAgent := "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" if capturedRequest.Header.Get("User-Agent") != expectedUserAgent { t.Fatalf("expected User-Agent %q, got %q", expectedUserAgent, capturedRequest.Header.Get("User-Agent")) } expectedAccept := "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" if capturedRequest.Header.Get("Accept") != expectedAccept { t.Fatalf("expected Accept %q, got %q", expectedAccept, capturedRequest.Header.Get("Accept")) } expectedAcceptLanguage := "en-US,en;q=0.5" if capturedRequest.Header.Get("Accept-Language") != expectedAcceptLanguage { t.Fatalf("expected Accept-Language %q, got %q", expectedAcceptLanguage, capturedRequest.Header.Get("Accept-Language")) } } func TestDNSCaching(t *testing.T) { svc := NewURLMetadataService() lookupCount := 0 mockResolver := &CountingMockDNSResolver{ MockDNSResolver: MockDNSResolver{ lookupResults: make(map[string][]net.IP), lookupErrors: make(map[string]error), }, lookupCount: &lookupCount, } mockResolver.SetLookupResult("example.com", []net.IP{net.ParseIP("8.8.8.8")}) svc.resolver = mockResolver svc.client = newTestClient(t, func(r *http.Request) (*http.Response, error) { body := io.NopCloser(strings.NewReader("Test Title")) header := make(http.Header) header.Set("Content-Type", "text/html; charset=utf-8") return &http.Response{StatusCode: http.StatusOK, Body: body, Header: header}, nil }) _, err := svc.FetchTitle(context.Background(), "https://example.com") if err != nil { t.Fatalf("unexpected error: %v", err) } if lookupCount != 1 { t.Fatalf("expected 1 DNS lookup, got %d", lookupCount) } _, err = svc.FetchTitle(context.Background(), "https://example.com") if err != nil { t.Fatalf("unexpected error: %v", err) } if lookupCount != 1 { t.Fatalf("expected 1 DNS lookup (cached), got %d", lookupCount) } } type CountingMockDNSResolver struct { MockDNSResolver lookupCount *int } func (c *CountingMockDNSResolver) LookupIP(hostname string) ([]net.IP, error) { *c.lookupCount++ return c.MockDNSResolver.LookupIP(hostname) } func TestIPv6PrivateRangeDetection(t *testing.T) { tests := []struct { name string ip string expected bool }{ {"fc00::1", "fc00::1", true}, {"fe80::1", "fe80::1", true}, {"ff00::1", "ff00::1", true}, {"::1", "::1", true}, {"2001:db8::1", "2001:db8::1", false}, {"2001:4860::1", "2001:4860::1", false}, {"2607:f8b0::1", "2607:f8b0::1", false}, {"invalid", "invalid", false}, {"", "", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var ip net.IP if tt.ip != "" && tt.ip != "invalid" { ip = net.ParseIP(tt.ip) } result := isPrivateIPv6(ip) if result != tt.expected { t.Fatalf("expected %v for IPv6 %q, got %v", tt.expected, tt.ip, result) } }) } } func TestIPRangeDetection(t *testing.T) { tests := []struct { name string ip string start string end string expected bool }{ {"IP in range", "192.168.1.100", "192.168.1.1", "192.168.1.255", true}, {"IP at start of range", "192.168.1.1", "192.168.1.1", "192.168.1.255", true}, {"IP at end of range", "192.168.1.255", "192.168.1.1", "192.168.1.255", true}, {"IP below range", "192.168.0.255", "192.168.1.1", "192.168.1.255", false}, {"IP above range", "192.168.2.1", "192.168.1.1", "192.168.1.255", false}, {"Same IP", "192.168.1.100", "192.168.1.100", "192.168.1.100", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ip := net.ParseIP(tt.ip) start := net.ParseIP(tt.start) end := net.ParseIP(tt.end) result := ipInRange(ip, start, end) if result != tt.expected { t.Fatalf("expected %v for IP %q in range %q-%q, got %v", tt.expected, tt.ip, tt.start, tt.end, result) } }) } } func TestIPv6RangeDetection(t *testing.T) { tests := []struct { name string ip string prefix []byte length int expected bool }{ {"fc00 prefix match", "fc00::1", []byte{0xfc, 0x00}, 7, true}, {"fc00 prefix no match", "fd00::1", []byte{0xfc, 0x00}, 7, true}, {"fe80 prefix match", "fe80::1", []byte{0xfe, 0x80}, 10, true}, {"fe80 prefix no match", "fe90::1", []byte{0xfe, 0x80}, 10, true}, {"ff00 prefix match", "ff00::1", []byte{0xff, 0x00}, 8, true}, {"ff00 prefix no match", "fe00::1", []byte{0xff, 0x00}, 8, false}, {"exact match", "::1", []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}, 128, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ip := net.ParseIP(tt.ip) result := ipv6InRange(ip, tt.prefix, tt.length) if result != tt.expected { t.Fatalf("expected %v for IPv6 %q with prefix %v/%d, got %v", tt.expected, tt.ip, tt.prefix, tt.length, result) } }) } } func TestFetchTitleWithDifferentStatusCodes(t *testing.T) { svc := NewURLMetadataService() tests := []struct { name string statusCode int expectErr bool }{ {"OK status", http.StatusOK, false}, {"Created status", http.StatusCreated, false}, {"Accepted status", http.StatusAccepted, false}, {"No Content status", http.StatusNoContent, false}, {"Bad Request", http.StatusBadRequest, true}, {"Unauthorized", http.StatusUnauthorized, true}, {"Forbidden", http.StatusForbidden, true}, {"Not Found", http.StatusNotFound, true}, {"Internal Server Error", http.StatusInternalServerError, true}, {"Bad Gateway", http.StatusBadGateway, true}, {"Service Unavailable", http.StatusServiceUnavailable, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { svc.client = newTestClient(t, func(r *http.Request) (*http.Response, error) { body := io.NopCloser(strings.NewReader("Test Title")) header := make(http.Header) header.Set("Content-Type", "text/html; charset=utf-8") return &http.Response{StatusCode: tt.statusCode, Body: body, Header: header}, nil }) mockResolver := NewMockDNSResolver() mockResolver.SetLookupResult("example.com", []net.IP{net.ParseIP("8.8.8.8")}) svc.resolver = mockResolver _, err := svc.FetchTitle(context.Background(), "https://example.com") if tt.expectErr { if err == nil { t.Fatal("expected error but got nil") } if !strings.Contains(err.Error(), "unexpected status code") { t.Fatalf("expected 'unexpected status code' error, got %v", err) } } else { if err != nil { t.Fatalf("unexpected error: %v", err) } } }) } } func TestFetchTitleWithBodyReadError(t *testing.T) { svc := NewURLMetadataService() svc.client = newTestClient(t, func(r *http.Request) (*http.Response, error) { errorReader := &errorReader{} body := io.NopCloser(errorReader) header := make(http.Header) header.Set("Content-Type", "text/html; charset=utf-8") return &http.Response{StatusCode: http.StatusOK, Body: body, Header: header}, nil }) mockResolver := NewMockDNSResolver() mockResolver.SetLookupResult("example.com", []net.IP{net.ParseIP("8.8.8.8")}) svc.resolver = mockResolver _, err := svc.FetchTitle(context.Background(), "https://example.com") if err == nil { t.Fatal("expected error but got nil") } if !strings.Contains(err.Error(), "read body") { t.Fatalf("expected 'read body' error, got %v", err) } } type errorReader struct{} func (e *errorReader) Read(p []byte) (n int, err error) { return 0, errors.New("read error") }