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 that no net/* package appears anywhere in the dependency graph of the non-test build, not only in its direct imports. 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 and replaces blogs.json only once that file parses as a non-empty JSON array of blog entries, so neither a truncated transfer nor a complete-but-wrong response such as an error page can overwrite the good dataset. It then runs the test suite against the new data. That target now needs jq as well as curl. blogs.json is an unmodified copy of a third party's file, redistributed here and in every binary that links the package, and the upstream repository publishes no licence. The README and the embed doc comment now record where it came from and that this repository's LICENSE does not extend to it. Whether that arrangement is acceptable is the owner's call. 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
152 lines
3.6 KiB
Go
152 lines
3.6 KiB
Go
package hnblogs
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"os/exec"
|
|
"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. 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")
|
|
}
|
|
}
|