Files
bucketeer/internal/index/index_test.go
T
StasonJatham a366905a43 Bucketeer UI (dark mode, pagination, virtual scroll), readable+indexed resources, LLM-friendly logs
Logging:
- structured slog with LOG_FORMAT=json|text and a stable key vocabulary
- periodic stats record with queue depths, db pool, goroutines, memory, dns/probe
- no per-certificate logging (hot path is counters only)

UI (Bucketeer branding, DESIGN.md):
- light+dark themes (OS default, persisted header toggle, no first-paint flash)
- official mascot: lossless WebP light/dark logos + favicon, embedded and served
  with cache headers (1.2MB source PNGs -> ~27/22KB webp)
- Assets/Contents views, pagination (offset) and native virtual scrolling
  (content-visibility) so large result sets do not blow up the DOM
- search with debounce/abort + URL state, exposure chips, sort, summary counts

Readability + indexing:
- internal/index listers for S3-compatible, GCS, Azure, Firebase, OpenSearch
  (metadata only: keys/sizes/dates; never object contents)
- asset_entries table + store write/search; readable resources are indexed and
  searchable in the UI
- deep, unauthenticated, read-only DB probes (PROBE_DEEP): Redis PING,
  PostgreSQL StartupMessage, MongoDB OP_MSG listDatabases; OpenSearch/Neptune
  HTTP reads
- Azure account-level listing fixed: 403 is reported reachable, not
  auth_required, since anonymous access is per-container

DNS:
- curated reliable resolvers by default; DNS_SERVERS=fresh adds the compiled-in
  Fresh-Resolvers snapshot with a bounded startup sample (warming all 6.7k at
  once broke the container resolver)
- random healthy selection + failover + per-server cooldown

Other: prober Close fixes a shutdown hang; config/README updated.
2026-09-22 14:33:03 +02:00

115 lines
4.1 KiB
Go

package index
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"tea.karlcom.de/karl/bucketeer/internal/model"
)
func newTestIndexer(handler http.Handler) (*Indexer, string) {
srv := httptest.NewServer(handler)
x := New(50, 2*time.Second)
x.scheme = "http"
x.gcsBase = srv.URL
return x, strings.TrimPrefix(srv.URL, "http://")
}
func TestListS3(t *testing.T) {
body := `<?xml version="1.0"?><ListBucketResult>
<IsTruncated>false</IsTruncated>
<CommonPrefixes><Prefix>photos/</Prefix></CommonPrefixes>
<Contents><Key>photos/a.jpg</Key><LastModified>2024-01-02T03:04:05.000Z</LastModified><ETag>"abc"</ETag><Size>1234</Size></Contents>
<Contents><Key>readme.txt</Key><Size>12</Size></Contents>
</ListBucketResult>`
x, host := newTestIndexer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("list-type") != "2" {
t.Errorf("expected list-type=2, got %q", r.URL.RawQuery)
}
w.Header().Set("Content-Type", "application/xml")
_, _ = w.Write([]byte(body))
}))
entries, err := x.List(context.Background(), model.Candidate{Hostname: host, Provider: model.ProviderAWS, Service: model.ServiceS3})
if err != nil {
t.Fatal(err)
}
if len(entries) != 3 {
t.Fatalf("got %d entries: %+v", len(entries), entries)
}
if entries[0].Kind != "prefix" || entries[0].Name != "photos/" {
t.Fatalf("prefix entry wrong: %+v", entries[0])
}
if entries[1].Name != "photos/a.jpg" || entries[1].Size != 1234 || entries[1].ETag != "abc" {
t.Fatalf("object entry wrong: %+v", entries[1])
}
if entries[1].Updated.IsZero() {
t.Fatal("expected updated timestamp parsed")
}
}
func TestListGCS(t *testing.T) {
body := `{"items":[{"name":"a/b.txt","size":"42","updated":"2024-05-06T07:08:09Z","contentType":"text/plain","etag":"e1"}],"prefixes":["a/"]}`
x, _ := newTestIndexer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.URL.Path, "/storage/v1/b/") {
t.Errorf("unexpected path %q", r.URL.Path)
}
_, _ = w.Write([]byte(body))
}))
entries, err := x.List(context.Background(), model.Candidate{Resource: "mybucket", Provider: model.ProviderGCP, Service: model.ServiceObjectStorage})
if err != nil {
t.Fatal(err)
}
if len(entries) != 2 || entries[1].Name != "a/b.txt" || entries[1].Size != 42 || entries[1].ContentType != "text/plain" {
t.Fatalf("unexpected entries: %+v", entries)
}
}
func TestListFirebase(t *testing.T) {
x, host := newTestIndexer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"users":true,"orders":true}`))
}))
entries, err := x.List(context.Background(), model.Candidate{Hostname: host, Provider: model.ProviderGCP, Service: model.ServiceFirebaseDB})
if err != nil {
t.Fatal(err)
}
if len(entries) != 2 || entries[0].Kind != "key" {
t.Fatalf("unexpected entries: %+v", entries)
}
}
func TestListOpenSearch(t *testing.T) {
x, host := newTestIndexer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`[{"index":"logs-2024","docs.count":"100","store.size":"1.2mb"}]`))
}))
entries, err := x.List(context.Background(), model.Candidate{Hostname: host, Provider: model.ProviderAWS, Service: model.ServiceOpenSearch})
if err != nil {
t.Fatal(err)
}
if len(entries) != 1 || entries[0].Name != "logs-2024" || entries[0].Size != 100 {
t.Fatalf("unexpected entries: %+v", entries)
}
}
func TestListBounded(t *testing.T) {
var keys strings.Builder
for i := 0; i < 100; i++ {
keys.WriteString("<Contents><Key>k")
keys.WriteString(string(rune('a' + i%26)))
keys.WriteString("</Key><Size>1</Size></Contents>")
}
body := `<ListBucketResult><IsTruncated>false</IsTruncated>` + keys.String() + `</ListBucketResult>`
x, host := newTestIndexer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(body)) }))
x.maxObjects = 10
entries, err := x.List(context.Background(), model.Candidate{Hostname: host, Provider: model.ProviderAWS, Service: model.ServiceS3})
if err != nil {
t.Fatal(err)
}
if len(entries) > 10 {
t.Fatalf("got %d entries, want <= 10", len(entries))
}
}