The library needed network access on first use and its results changed under the caller between runs. blogs.json is now vendored and compiled in with go:embed, so the dataset is fixed for a given build. FetchBlogs keeps its name, signature and sync.Once memoization but now decodes the embedded bytes; its error return is only reachable if the committed blogs.json is malformed. net/http is gone from the package, and a test asserts no net/* import returns to non-test code. make update-data refreshes the vendored file, reading the upstream location from the BlogsURL constant so the URL has one definition. It downloads to a temporary file, replaces blogs.json only on a complete download, and runs the test suite against the new data. The dataset is committed verbatim as upstream serves it, which is ~8 MB of JSON in the repo and in every linking binary; most of that is per-blog post history that the Blog struct does not expose. Model: opus-5
143 lines
3.3 KiB
Go
143 lines
3.3 KiB
Go
package hnblogs
|
|
|
|
import (
|
|
"encoding/json"
|
|
"go/build"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestFetchBlogs(t *testing.T) {
|
|
blogs, err := FetchBlogs()
|
|
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. A
|
|
// transport dependency reintroduced in non-test code fails here.
|
|
func TestNoRuntimeNetworkImports(t *testing.T) {
|
|
pkg, err := build.ImportDir(".", 0)
|
|
if err != nil {
|
|
t.Fatalf("Failed to inspect package imports: %v", err)
|
|
}
|
|
|
|
for _, imported := range pkg.Imports {
|
|
if imported == "net" || strings.HasPrefix(imported, "net/") {
|
|
t.Fatalf("Package must not perform network I/O, but imports %q", imported)
|
|
}
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|