package hnblogs import ( "bytes" "encoding/json" "os/exec" "strings" "testing" ) func TestGetBlogs(t *testing.T) { blogs, err := GetBlogs() if err != nil { t.Fatalf("Expected no error, got %v", err) } if len(blogs) == 0 { t.Fatalf("Expected the embedded dataset to be non-empty, got %d blogs", len(blogs)) } } // TestEmbeddedDataIsUsable guards the invariant the embedded dataset has to // satisfy for the rest of the package: every entry has a URL, so callers of // RandomBlog and NthBlog get something dereferenceable. func TestEmbeddedDataIsUsable(t *testing.T) { if !json.Valid(blogsJSON) { t.Fatalf("Embedded blogs.json is not valid JSON") } blogs, err := GetBlogs() if err != nil { t.Fatalf("Expected no error, got %v", err) } for i, blog := range blogs { if blog.URL == "" { t.Fatalf("Blog at index %d has an empty URL", i) } } } // TestNoRuntimeNetworkImports is the regression guard for the reason this data // is embedded: the package must not reach the network on any code path. It // walks the whole dependency graph of the non-test build rather than the direct // import list, so a net/* package reached through an intermediate import fails // here too. Test-only imports are outside that graph, which is why this test // may use os/exec itself. func TestNoRuntimeNetworkImports(t *testing.T) { cmd := exec.Command("go", "list", "-deps", ".") var stderr bytes.Buffer cmd.Stderr = &stderr out, err := cmd.Output() if err != nil { t.Fatalf("Failed to list package dependencies: %v\n%s", err, stderr.String()) } for _, dep := range strings.Fields(string(out)) { if dep == "net" || strings.HasPrefix(dep, "net/") { t.Fatalf("Package must not perform network I/O, but depends on %q", dep) } } } func TestGetBlogsIsMemoized(t *testing.T) { first, err := GetBlogs() if err != nil { t.Fatalf("Expected no error, got %v", err) } second, err := GetBlogs() if err != nil { t.Fatalf("Expected no error, got %v", err) } if len(first) != len(second) { t.Fatalf("Expected the same slice on repeated calls, got %d then %d", len(first), len(second)) } if len(first) > 0 && &first[0] != &second[0] { t.Fatalf("Expected repeated calls to share the memoized backing array") } } func TestRandomBlog(t *testing.T) { blog, err := RandomBlog() if err != nil { t.Fatalf("Expected no error, got %v", err) } if blog.URL == "" { t.Fatalf("Expected a valid blog, got an empty URL") } } func TestRandomBlogs(t *testing.T) { n := 5 randomBlogs, err := RandomBlogs(n) if err != nil { t.Fatalf("Expected no error, got %v", err) } if len(randomBlogs) != n { t.Fatalf("Expected %d random blogs, got %d", n, len(randomBlogs)) } for _, blog := range randomBlogs { if blog.URL == "" { t.Fatalf("Expected a valid blog, got an empty URL") } } } func TestRandomBlogsRejectsInvalidCounts(t *testing.T) { blogs, err := GetBlogs() if err != nil { t.Fatalf("Expected no error, got %v", err) } for _, n := range []int{0, -1, len(blogs) + 1} { if _, err := RandomBlogs(n); err == nil { t.Fatalf("Expected an error for n=%d, got none", n) } } } func TestNthBlog(t *testing.T) { blogs, err := GetBlogs() if err != nil { t.Fatalf("Expected no error, got %v", err) } blog, err := NthBlog(5) if err != nil { t.Fatalf("Expected no error, got %v", err) } if blog.URL != blogs[5].URL { t.Fatalf("Expected %s, got %s", blogs[5].URL, blog.URL) } _, err = NthBlog(len(blogs)) if err == nil { t.Fatalf("Expected error for out-of-range index, got none") } if _, err := NthBlog(-1); err == nil { t.Fatalf("Expected error for negative index, got none") } }