mirror of
https://github.com/therootcompany/golib.git
synced 2026-11-04 06:55:00 +00:00
221 lines
6.1 KiB
Go
221 lines
6.1 KiB
Go
package geoip
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// writeConf writes a minimal GeoIP.conf with the given edition IDs to dir.
|
|
func writeConf(t *testing.T, dir string, editions ...string) string {
|
|
t.Helper()
|
|
conf := "AccountID 123456\nLicenseKey testkey\nEditionIDs " +
|
|
joinSpace(editions) + "\n"
|
|
p := filepath.Join(dir, "GeoIP.conf")
|
|
if err := os.WriteFile(p, []byte(conf), 0o600); err != nil {
|
|
t.Fatalf("write conf: %v", err)
|
|
}
|
|
return p
|
|
}
|
|
|
|
func joinSpace(ss []string) string {
|
|
var s strings.Builder
|
|
for i, x := range ss {
|
|
if i > 0 {
|
|
s.WriteString(" ")
|
|
}
|
|
s.WriteString(x)
|
|
}
|
|
return s.String()
|
|
}
|
|
|
|
// TestGeoIPDBDownloadAuth verifies the first fetch issues a Basic-auth GET,
|
|
// writes the tarball to disk, and reports updated=true. A second fetch within
|
|
// MaxAge skips HTTP entirely (server hit count stays at 1).
|
|
func TestGeoIPDBDownloadAuth(t *testing.T) {
|
|
const (
|
|
user = "123456"
|
|
key = "testkey"
|
|
)
|
|
var hits int32
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
atomic.AddInt32(&hits, 1)
|
|
got := r.Header.Get("Authorization")
|
|
want := "Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+key))
|
|
if got != want {
|
|
http.Error(w, "bad auth", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
if r.URL.Path != "/GeoLite2-City/download" {
|
|
http.Error(w, "bad path "+r.URL.Path, http.StatusBadRequest)
|
|
return
|
|
}
|
|
w.Header().Set("ETag", `"v1"`)
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte("TARGZ-BODY-CITY"))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
dir := t.TempDir()
|
|
confPath := writeConf(t, dir, "GeoLite2-City")
|
|
|
|
f, err := NewGeoIPDB(confPath)
|
|
if err != nil {
|
|
t.Fatalf("NewGeoIPDB: %v", err)
|
|
}
|
|
f.Dir = dir
|
|
f.BaseURL = srv.URL
|
|
f.HTTPClient = srv.Client()
|
|
|
|
updated, err := f.Update(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("Fetch #1: %v", err)
|
|
}
|
|
if !updated {
|
|
t.Fatal("Fetch #1: expected updated=true")
|
|
}
|
|
if got := atomic.LoadInt32(&hits); got != 1 {
|
|
t.Fatalf("Fetch #1: server hits = %d, want 1", got)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dir, TarGzName("GeoLite2-City"))); err != nil {
|
|
t.Fatalf("tarball not written: %v", err)
|
|
}
|
|
|
|
// Second fetch within MaxAge (default 3 days) should skip HTTP.
|
|
updated, err = f.Update(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("Fetch #2: %v", err)
|
|
}
|
|
if updated {
|
|
t.Fatal("Fetch #2: expected updated=false within MaxAge")
|
|
}
|
|
if got := atomic.LoadInt32(&hits); got != 1 {
|
|
t.Fatalf("Fetch #2: server hits = %d, want 1 (MaxAge gate)", got)
|
|
}
|
|
}
|
|
|
|
// TestGeoIPDBConditionalGET verifies the conditional-GET flow: a stale local
|
|
// file triggers an If-None-Match GET. A 304 reports updated=false; a 200 with
|
|
// a new ETag reports updated=true and rewrites the file.
|
|
func TestGeoIPDBConditionalGET(t *testing.T) {
|
|
var hits, condHits int32
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
atomic.AddInt32(&hits, 1)
|
|
switch r.Header.Get("If-None-Match") {
|
|
case `"v1"`:
|
|
// First conditional request: not modified.
|
|
if atomic.AddInt32(&condHits, 1) == 1 {
|
|
w.WriteHeader(http.StatusNotModified)
|
|
return
|
|
}
|
|
// Server changed its mind: ship v2.
|
|
w.Header().Set("ETag", `"v2"`)
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte("TARGZ-CITY-V2"))
|
|
default:
|
|
// Prime: offer v1.
|
|
w.Header().Set("ETag", `"v1"`)
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte("TARGZ-CITY-V1"))
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
dir := t.TempDir()
|
|
confPath := writeConf(t, dir, "GeoLite2-City")
|
|
|
|
f, err := NewGeoIPDB(confPath)
|
|
if err != nil {
|
|
t.Fatalf("NewGeoIPDB: %v", err)
|
|
}
|
|
f.Dir = dir
|
|
f.BaseURL = srv.URL
|
|
f.HTTPClient = srv.Client()
|
|
|
|
// Prime with v1.
|
|
if updated, err := f.Update(context.Background()); err != nil || !updated {
|
|
t.Fatalf("prime: updated=%v err=%v", updated, err)
|
|
}
|
|
if got := atomic.LoadInt32(&hits); got != 1 {
|
|
t.Fatalf("prime hits = %d, want 1", got)
|
|
}
|
|
|
|
// Backdate the local file so MaxAge no longer gates the next fetch.
|
|
p := filepath.Join(dir, TarGzName("GeoLite2-City"))
|
|
old := time.Now().Add(-4 * 24 * time.Hour)
|
|
if err := os.Chtimes(p, old, old); err != nil {
|
|
t.Fatalf("Chtimes: %v", err)
|
|
}
|
|
|
|
// Server now returns 304 (not modified) for the conditional request.
|
|
if updated, err := f.Update(context.Background()); err != nil || updated {
|
|
t.Fatalf("304 path: updated=%v err=%v", updated, err)
|
|
}
|
|
if got := atomic.LoadInt32(&hits); got != 2 {
|
|
t.Fatalf("after 304 hits = %d, want 2", got)
|
|
}
|
|
|
|
// Server now returns 200 with the new body/ETag.
|
|
if updated, err := f.Update(context.Background()); err != nil || !updated {
|
|
t.Fatalf("200 path: updated=%v err=%v", updated, err)
|
|
}
|
|
if got := atomic.LoadInt32(&hits); got != 3 {
|
|
t.Fatalf("after 200 hits = %d, want 3", got)
|
|
}
|
|
b, _ := os.ReadFile(p)
|
|
if string(b) != "TARGZ-CITY-V2" {
|
|
t.Fatalf("body = %q, want TARGZ-CITY-V2", string(b))
|
|
}
|
|
}
|
|
|
|
// TestGeoIPDBDefaultFreshDays ensures a zero FreshDays defaults to a 3-day
|
|
// MaxAge gate.
|
|
func TestGeoIPDBDefaultFreshDays(t *testing.T) {
|
|
dir := t.TempDir()
|
|
confPath := writeConf(t, dir, "GeoLite2-City")
|
|
f, err := NewGeoIPDB(confPath)
|
|
if err != nil {
|
|
t.Fatalf("NewGeoIPDB: %v", err)
|
|
}
|
|
if err := f.ready(); err != nil {
|
|
t.Fatalf("ready: %v", err)
|
|
}
|
|
if want := 3 * 24 * time.Hour; f.maxAge != want {
|
|
t.Fatalf("maxAge = %v, want %v", f.maxAge, want)
|
|
}
|
|
if len(f.cachers) != 1 {
|
|
t.Fatalf("cachers = %d, want 1", len(f.cachers))
|
|
}
|
|
}
|
|
|
|
// TestGeoIPDBExplicitFreshDays checks a non-default FreshDays is applied.
|
|
func TestGeoIPDBExplicitFreshDays(t *testing.T) {
|
|
dir := t.TempDir()
|
|
confPath := writeConf(t, dir, "GeoLite2-City")
|
|
f, err := NewGeoIPDB(confPath)
|
|
if err != nil {
|
|
t.Fatalf("NewGeoIPDB: %v", err)
|
|
}
|
|
f.FreshDays = 10
|
|
if err := f.ready(); err != nil {
|
|
t.Fatalf("ready: %v", err)
|
|
}
|
|
if want := 10 * 24 * time.Hour; f.maxAge != want {
|
|
t.Fatalf("maxAge = %v, want %v", f.maxAge, want)
|
|
}
|
|
}
|
|
|
|
// TestGeoIPDBMissingConf ensures a missing conf file returns an error.
|
|
func TestGeoIPDBMissingConf(t *testing.T) {
|
|
if _, err := NewGeoIPDB(filepath.Join(t.TempDir(), "nope.conf")); err == nil {
|
|
t.Fatal("expected error for missing conf")
|
|
}
|
|
}
|