main pico / pkg / apps / pgs / cli_test.go
Antonio Mika  ·  2026-10-10
  1package pgs
  2
  3import (
  4	"bytes"
  5	"io"
  6	"log/slog"
  7	"os"
  8	"path/filepath"
  9	"strings"
 10	"testing"
 11
 12	"github.com/picosh/pico/pkg/shared"
 13	"github.com/picosh/pico/pkg/storage"
 14)
 15
 16type fakeCmdSession struct{ bytes.Buffer }
 17
 18func (s *fakeCmdSession) Exit(int) error        { return nil }
 19func (s *fakeCmdSession) Close() error          { return nil }
 20func (s *fakeCmdSession) Stderr() io.ReadWriter { return &s.Buffer }
 21
 22// Commands that change how a project is served purge its cached pages and
 23// records.
 24func TestCliPurges(t *testing.T) {
 25	logger := slog.New(slog.DiscardHandler)
 26	dbpool := NewPgsDb(logger)
 27	user := dbpool.Users[0]
 28	if _, err := dbpool.InsertProject(user.ID, "other", "other"); err != nil {
 29		t.Fatal(err)
 30	}
 31	st, err := storage.NewStorageMemory(map[string]map[string]string{
 32		shared.GetAssetBucketName(user.ID): {"/test/index.html": "hello"},
 33	})
 34	if err != nil {
 35		t.Fatal(err)
 36	}
 37	cfg := NewConfig(logger, dbpool, st, discardPubsub{})
 38	cmd := &Cmd{User: user, Session: &fakeCmdSession{}, Log: logger, Store: st, Dbpool: dbpool, Write: true, Cfg: cfg}
 39
 40	tests := []struct {
 41		name string
 42		run  func() error
 43	}{
 44		{"acl", func() error { return cmd.acl("test", "private", nil) }},
 45		{"link", func() error { return cmd.link("test", "other") }},
 46		{"unlink", func() error { return cmd.unlink("test") }},
 47		{"rm", func() error { return cmd.rm("test") }},
 48	}
 49	want := getSurrogateKey(user.Name, "test")
 50	for _, tt := range tests {
 51		if err := tt.run(); err != nil {
 52			t.Fatalf("%s: %v", tt.name, err)
 53		}
 54		select {
 55		case got := <-cfg.CacheClearingQueue:
 56			if got != want {
 57				t.Fatalf("%s purged %q, want %q", tt.name, got, want)
 58			}
 59		default:
 60			t.Fatalf("%s purged nothing", tt.name)
 61		}
 62		for len(cfg.CacheClearingQueue) > 0 {
 63			<-cfg.CacheClearingQueue
 64		}
 65	}
 66}
 67
 68// Removing a project's assets also removes its directories, which storage
 69// keeps after their last file is deleted.
 70func TestRmProjectAssetsRemovesDirs(t *testing.T) {
 71	logger := slog.New(slog.DiscardHandler)
 72	dbpool := NewPgsDb(logger)
 73	user := dbpool.Users[0]
 74	st, err := storage.NewStorageFS(logger, t.TempDir())
 75	if err != nil {
 76		t.Fatal(err)
 77	}
 78	bucket, err := st.UpsertBucket(shared.GetAssetBucketName(user.ID))
 79	if err != nil {
 80		t.Fatal(err)
 81	}
 82	for _, name := range []string{"test/index.html", "test/css/deep/site.css", "other/index.html"} {
 83		if _, _, err := st.PutObject(bucket, name, strings.NewReader("x"), &storage.ObjectInfo{}); err != nil {
 84			t.Fatal(err)
 85		}
 86	}
 87	if err := st.PutDir(bucket, "test/empty/nested"); err != nil {
 88		t.Fatal(err)
 89	}
 90
 91	cfg := NewConfig(logger, dbpool, st, discardPubsub{})
 92	cmd := &Cmd{User: user, Session: &fakeCmdSession{}, Log: logger, Store: st, Dbpool: dbpool, Write: true, Cfg: cfg}
 93	if err := cmd.RmProjectAssets("test"); err != nil {
 94		t.Fatal(err)
 95	}
 96	if _, err := os.Stat(filepath.Join(bucket.Path, "test")); !os.IsNotExist(err) {
 97		t.Fatalf("project directory still exists: %v", err)
 98	}
 99	if _, err := os.Stat(filepath.Join(bucket.Path, "other/index.html")); err != nil {
100		t.Fatalf("another project was touched: %v", err)
101	}
102}