package fetch import ( "context" "crypto/sha256" "encoding/hex" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "sync" "testing" "git.t5de.dev/t5de/t5de-api/pack" "git.t5de.dev/t5de/t5de-proxy/internal/hub" ) const id = "abcdefgh23" func indexXML(rating string) []byte { return []byte(``) } func entry(name string, data []byte) hub.FileEntry { sum := sha256.Sum256(data) return hub.FileEntry{Name: name, SHA256: hex.EncodeToString(sum[:]), Size: int64(len(data))} } // fakeHub answers for one product. type fakeHub struct { mu sync.Mutex files hub.Files } func (h *fakeHub) ProductFiles(ctx context.Context, pid string) (*hub.Files, error) { h.mu.Lock() defer h.mu.Unlock() f := h.files return &f, nil } func (h *fakeHub) AccessToken(ctx context.Context) (string, error) { return "adult-token", nil } func (h *fakeHub) Library(ctx context.Context) ([]hub.LibraryEntry, error) { var e hub.LibraryEntry e.Product.ID = id return []hub.LibraryEntry{e}, nil } // catalog serves blobs by sha256; adult ones need the token. func catalog(t *testing.T, blobs map[string][]byte, adult map[string]bool, served *int) *httptest.Server { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { sum := strings.TrimPrefix(r.URL.Path, "/api/v1/files/") b, ok := blobs[sum] if !ok { http.NotFound(w, r) return } if adult[sum] && r.Header.Get("Authorization") != "Bearer adult-token" { w.WriteHeader(http.StatusUnauthorized) return } *served++ w.Write(b) })) t.Cleanup(srv.Close) return srv } func TestEnsureWritesAPack(t *testing.T) { index, tex := indexXML("GA"), []byte("texture one") tex2 := []byte("texture two") blobs := map[string][]byte{} for _, b := range [][]byte{index, tex, tex2} { blobs[entry("", b).SHA256] = b } served := 0 cat := catalog(t, blobs, nil, &served) h := &fakeHub{files: hub.Files{Product: id, Revision: 1, Rating: "general", Name: "Blue Coat", Creator: "maker", Files: []hub.FileEntry{entry("index.xml", index), entry("tex.png", tex)}, Catalogs: []string{"http://127.0.0.1:1", cat.URL}}} dir := t.TempDir() f := New(h, dir, t.Logf) var ready []string f.OnReady = func(id string) { ready = append(ready, id) } f.SyncLibrary(context.Background()) p, err := pack.Load(filepath.Join(dir, id)) if err != nil { t.Fatal(err) } if p.Manifest.Name != "Blue Coat" || p.Manifest.Revision != 1 || len(p.Files) != 2 || len(ready) != 1 { t.Fatalf("pack %+v, ready %v", p.Manifest, ready) } // Already there: nothing fetched again. if err := f.Ensure(context.Background(), id); err != nil || served != 2 || len(ready) != 1 { t.Fatalf("again: %v, served %d", err, served) } // A new revision: only the changed file is downloaded; the pack is replaced. h.files.Revision, h.files.Files = 2, []hub.FileEntry{entry("index.xml", index), entry("tex.png", tex2)} if err := f.Ensure(context.Background(), id); err != nil { t.Fatal(err) } if b, _ := os.ReadFile(filepath.Join(dir, id, "files", "tex.png")); string(b) != "texture two" || served != 3 { t.Fatalf("tex %q, served %d", b, served) } entries, _ := os.ReadDir(dir) for _, e := range entries { if e.Name() != id && e.Name() != ".blobs" { t.Fatalf("left behind %s", e.Name()) } } } func TestEnsureChecksFilesAndAdultTokens(t *testing.T) { index := indexXML("AP") blobs := map[string][]byte{entry("", index).SHA256: index} adult := map[string]bool{entry("", index).SHA256: true} served := 0 cat := catalog(t, blobs, adult, &served) h := &fakeHub{files: hub.Files{Product: id, Revision: 1, Rating: "adult", Name: "Lace", Files: []hub.FileEntry{entry("index.xml", index)}, Catalogs: []string{cat.URL}}} f := New(h, t.TempDir(), t.Logf) if err := f.Ensure(context.Background(), id); err != nil { t.Fatal(err) } // A catalog sending something else than the hub listed is refused. h.files.Revision = 2 h.files.Files = []hub.FileEntry{{Name: "index.xml", SHA256: strings.Repeat("0", 64), Size: int64(len(index))}} blobs[strings.Repeat("0", 64)] = []byte(strings.Repeat("x", len(index))) if err := f.Ensure(context.Background(), id); err == nil || !strings.Contains(err.Error(), "different file") { t.Fatalf("got %v", err) } // File names that would leave the pack's folder are refused. h.files.Files = []hub.FileEntry{{Name: `..\evil`, SHA256: strings.Repeat("0", 64), Size: 1}} if err := f.Ensure(context.Background(), id); err == nil || !strings.Contains(err.Error(), "bad file name") { t.Fatalf("got %v", err) } if err := f.Ensure(context.Background(), "../etc"); err == nil { t.Fatal("bad ID accepted") } }