diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c51a306c..6ce26510 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,7 +34,7 @@ jobs: args: -E goimports -E godot --timeout 10m - name: Run tests run: | - go test -v ./... -cover -race -coverprofile=coverage.out + PICO_SECRET="danger" go test -v ./... -cover -race -coverprofile=coverage.out go tool cover -func=coverage.out -o=coverage.out build-main: runs-on: ubuntu-22.04 diff --git a/auth/api.go b/auth/api.go index 0b4a0d6e..8e279c43 100644 --- a/auth/api.go +++ b/auth/api.go @@ -613,7 +613,7 @@ func createMainRoutes() []shared.Route { return routes } -func handler(routes []shared.Route, client *Client) shared.ServeFn { +func handler(routes []shared.Route, client *Client) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var allow []string diff --git a/db/stub/stub.go b/db/stub/stub.go new file mode 100644 index 00000000..145e87b3 --- /dev/null +++ b/db/stub/stub.go @@ -0,0 +1,282 @@ +package stub + +import ( + "database/sql" + "fmt" + "log/slog" + "time" + + "github.com/picosh/pico/db" +) + +type StubDB struct { + Logger *slog.Logger +} + +var _ db.DB = (*StubDB)(nil) + +func NewStubDB(logger *slog.Logger) *StubDB { + d := &StubDB{ + Logger: logger, + } + d.Logger.Info("Connecting to test database") + return d +} + +var notImpl = fmt.Errorf("not implemented") + +func (me *StubDB) RegisterUser(username, pubkey, comment string) (*db.User, error) { + return nil, notImpl +} + +func (me *StubDB) RemoveUsers(userIDs []string) error { + return notImpl +} + +func (me *StubDB) InsertPublicKey(userID, key, name string, tx *sql.Tx) error { + return notImpl +} + +func (me *StubDB) UpdatePublicKey(pubkeyID, name string) (*db.PublicKey, error) { + return nil, notImpl +} + +func (me *StubDB) FindPublicKeyForKey(key string) (*db.PublicKey, error) { + return nil, notImpl +} + +func (me *StubDB) FindPublicKey(pubkeyID string) (*db.PublicKey, error) { + return nil, notImpl +} + +func (me *StubDB) FindKeysForUser(user *db.User) ([]*db.PublicKey, error) { + return []*db.PublicKey{}, notImpl +} + +func (me *StubDB) RemoveKeys(keyIDs []string) error { + return notImpl +} + +func (me *StubDB) FindSiteAnalytics(space string) (*db.Analytics, error) { + return nil, notImpl +} + +func (me *StubDB) FindPostsBeforeDate(date *time.Time, space string) ([]*db.Post, error) { + return []*db.Post{}, notImpl +} + +func (me *StubDB) FindUserForKey(username string, key string) (*db.User, error) { + return nil, notImpl +} + +func (me *StubDB) FindUser(userID string) (*db.User, error) { + return nil, notImpl +} + +func (me *StubDB) ValidateName(name string) (bool, error) { + return false, notImpl +} + +func (me *StubDB) FindUserForName(name string) (*db.User, error) { + return nil, notImpl +} + +func (me *StubDB) FindUserForNameAndKey(name string, key string) (*db.User, error) { + return nil, notImpl +} + +func (me *StubDB) FindUserForToken(token string) (*db.User, error) { + return nil, notImpl +} + +func (me *StubDB) SetUserName(userID string, name string) error { + return notImpl +} + +func (me *StubDB) FindPostWithFilename(filename string, persona_id string, space string) (*db.Post, error) { + return nil, notImpl +} + +func (me *StubDB) FindPostWithSlug(slug string, user_id string, space string) (*db.Post, error) { + return nil, notImpl +} + +func (me *StubDB) FindPost(postID string) (*db.Post, error) { + return nil, notImpl +} + +func (me *StubDB) FindAllPosts(page *db.Pager, space string) (*db.Paginate[*db.Post], error) { + return &db.Paginate[*db.Post]{}, notImpl +} + +func (me *StubDB) FindAllUpdatedPosts(page *db.Pager, space string) (*db.Paginate[*db.Post], error) { + return &db.Paginate[*db.Post]{}, notImpl +} + +func (me *StubDB) InsertPost(post *db.Post) (*db.Post, error) { + return nil, notImpl +} + +func (me *StubDB) UpdatePost(post *db.Post) (*db.Post, error) { + return nil, notImpl +} + +func (me *StubDB) RemovePosts(postIDs []string) error { + return notImpl +} + +func (me *StubDB) FindPostsForUser(page *db.Pager, userID string, space string) (*db.Paginate[*db.Post], error) { + return &db.Paginate[*db.Post]{}, notImpl +} + +func (me *StubDB) FindAllPostsForUser(userID string, space string) ([]*db.Post, error) { + return []*db.Post{}, notImpl +} + +func (me *StubDB) FindPosts() ([]*db.Post, error) { + return []*db.Post{}, notImpl +} + +func (me *StubDB) FindExpiredPosts(space string) ([]*db.Post, error) { + return []*db.Post{}, notImpl +} + +func (me *StubDB) FindUpdatedPostsForUser(userID string, space string) ([]*db.Post, error) { + return []*db.Post{}, notImpl +} + +func (me *StubDB) Close() error { + return notImpl +} + +func (me *StubDB) InsertVisit(view *db.AnalyticsVisits) error { + return notImpl +} + +func (me *StubDB) VisitSummary(opts *db.SummaryOpts) (*db.SummaryVisits, error) { + return &db.SummaryVisits{}, notImpl +} + +func (me *StubDB) FindUsers() ([]*db.User, error) { + return []*db.User{}, notImpl +} + +func (me *StubDB) ReplaceTagsForPost(tags []string, postID string) error { + return notImpl +} + +func (me *StubDB) ReplaceAliasesForPost(aliases []string, postID string) error { + return notImpl +} + +func (me *StubDB) FindUserPostsByTag(page *db.Pager, tag, userID, space string) (*db.Paginate[*db.Post], error) { + return &db.Paginate[*db.Post]{}, notImpl +} + +func (me *StubDB) FindPostsByTag(pager *db.Pager, tag, space string) (*db.Paginate[*db.Post], error) { + return &db.Paginate[*db.Post]{}, notImpl +} + +func (me *StubDB) FindPopularTags(space string) ([]string, error) { + return []string{}, notImpl +} + +func (me *StubDB) FindTagsForPost(postID string) ([]string, error) { + return []string{}, notImpl +} + +func (me *StubDB) FindFeatureForUser(userID string, feature string) (*db.FeatureFlag, error) { + return nil, notImpl +} + +func (me *StubDB) FindFeaturesForUser(userID string) ([]*db.FeatureFlag, error) { + return []*db.FeatureFlag{}, notImpl +} + +func (me *StubDB) HasFeatureForUser(userID string, feature string) bool { + return false +} + +func (me *StubDB) FindTotalSizeForUser(userID string) (int, error) { + return 0, notImpl +} + +func (me *StubDB) InsertFeedItems(postID string, items []*db.FeedItem) error { + return notImpl +} + +func (me *StubDB) FindFeedItemsByPostID(postID string) ([]*db.FeedItem, error) { + return []*db.FeedItem{}, notImpl +} + +func (me *StubDB) InsertProject(userID, name, projectDir string) (string, error) { + return "", notImpl +} + +func (me *StubDB) UpdateProject(userID, name string) error { + return notImpl +} + +func (me *StubDB) UpdateProjectAcl(userID, name string, acl db.ProjectAcl) error { + return notImpl +} + +func (me *StubDB) LinkToProject(userID, projectID, projectDir string, commit bool) error { + return notImpl +} + +func (me *StubDB) RemoveProject(projectID string) error { + return notImpl +} + +func (me *StubDB) FindProjectByName(userID, name string) (*db.Project, error) { + return &db.Project{}, notImpl +} + +func (me *StubDB) FindProjectLinks(userID, name string) ([]*db.Project, error) { + return []*db.Project{}, notImpl +} + +func (me *StubDB) FindProjectsByPrefix(userID, prefix string) ([]*db.Project, error) { + return []*db.Project{}, notImpl +} + +func (me *StubDB) FindProjectsByUser(userID string) ([]*db.Project, error) { + return []*db.Project{}, notImpl +} + +func (me *StubDB) FindAllProjects(page *db.Pager, by string) (*db.Paginate[*db.Project], error) { + return &db.Paginate[*db.Project]{}, notImpl +} + +func (me *StubDB) InsertToken(userID, name string) (string, error) { + return "", notImpl +} + +func (me *StubDB) UpsertToken(userID, name string) (string, error) { + return "", notImpl +} + +func (me *StubDB) FindTokenByName(userID, name string) (string, error) { + return "", notImpl +} + +func (me *StubDB) RemoveToken(tokenID string) error { + return notImpl +} + +func (me *StubDB) FindTokensForUser(userID string) ([]*db.Token, error) { + return []*db.Token{}, notImpl +} + +func (me *StubDB) InsertFeature(userID, name string, expiresAt time.Time) (*db.FeatureFlag, error) { + return nil, notImpl +} + +func (me *StubDB) RemoveFeature(userID string, name string) error { + return notImpl +} + +func (me *StubDB) AddPicoPlusUser(username, paymentType, txId string) error { + return notImpl +} diff --git a/go.mod b/go.mod index c389ae00..417ea98b 100644 --- a/go.mod +++ b/go.mod @@ -36,7 +36,7 @@ require ( github.com/muesli/reflow v0.3.0 github.com/muesli/termenv v0.15.3-0.20240912151726-82936c5ea257 github.com/neurosnap/go-exif-remove v0.0.0-20221010134343-50d1e3c35577 - github.com/picosh/pobj v0.0.0-20241008013754-bbbfc341e2cf + github.com/picosh/pobj v0.0.0-20241016194248-c39198b2ff23 github.com/picosh/pubsub v0.0.0-20241008010300-a63fd95dc8ed github.com/picosh/send v0.0.0-20241008013240-6fdbff00f848 github.com/picosh/tunkit v0.0.0-20240709033345-8315d4f3cd0e diff --git a/go.sum b/go.sum index bbaf8b6a..87f3e9ef 100644 --- a/go.sum +++ b/go.sum @@ -265,8 +265,8 @@ github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1Gsh github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/picosh/go-rsync-receiver v0.0.0-20240709135253-1daf4b12a9fc h1:bvcsoOvaNHPquFnRkdraEo7+8t6bW7nWEhlALnwZPdI= github.com/picosh/go-rsync-receiver v0.0.0-20240709135253-1daf4b12a9fc/go.mod h1:i0iR3W4GSm1PuvVxB9OH32E5jP+CYkVb2NQSe0JCtlo= -github.com/picosh/pobj v0.0.0-20241008013754-bbbfc341e2cf h1:Ul+LuTVXRimpIneOHez05k7VOV/lDVw37I18rceEplw= -github.com/picosh/pobj v0.0.0-20241008013754-bbbfc341e2cf/go.mod h1:cF+eAl4G1vU+WOD8cYCKaxokHo6MWmbR8J4/SJnvESg= +github.com/picosh/pobj v0.0.0-20241016194248-c39198b2ff23 h1:NEJ5a4UXeF0/X7xmYNzXcwLQID9DwgazlqkMMC5zZ3M= +github.com/picosh/pobj v0.0.0-20241016194248-c39198b2ff23/go.mod h1:cF+eAl4G1vU+WOD8cYCKaxokHo6MWmbR8J4/SJnvESg= github.com/picosh/pubsub v0.0.0-20241008010300-a63fd95dc8ed h1:aBJeQoLvq/V3hX6bgWjuuTmGzgbPNYuuwaCWU4aSJcU= github.com/picosh/pubsub v0.0.0-20241008010300-a63fd95dc8ed/go.mod h1:ajolgob5MxlHdp5HllF7u3rTlCgER4InqfP7M/xl6HQ= github.com/picosh/send v0.0.0-20241008013240-6fdbff00f848 h1:VWbjNNOqpJ8AB3zdw+M5+XC/SINooWLGi6WCozKwt1o= diff --git a/pgs/api.go b/pgs/api.go index 47b5caeb..baf22f66 100644 --- a/pgs/api.go +++ b/pgs/api.go @@ -249,6 +249,7 @@ func (h *AssetHandler) handle(logger *slog.Logger, w http.ResponseWriter, r *htt attempts = append(attempts, fp.Filepath) mimeType := storage.GetMimeType(fp.Filepath) + logger = logger.With("filename", fp.Filepath) var c io.ReadCloser var err error if strings.HasPrefix(mimeType, "image/") { diff --git a/pgs/api_test.go b/pgs/api_test.go new file mode 100644 index 00000000..7e2afd6c --- /dev/null +++ b/pgs/api_test.go @@ -0,0 +1,119 @@ +package pgs + +import ( + "fmt" + "log/slog" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/picosh/pico/db" + "github.com/picosh/pico/db/stub" + "github.com/picosh/pico/shared" + "github.com/picosh/pico/shared/storage" +) + +var testUserID = "user-1" +var testUsername = "user" + +type ApiExample struct { + name string + path string + want string + status int + contentType string + + dbpool db.DB + storage map[string]map[string]string +} + +type PgsDb struct { + *stub.StubDB +} + +func NewPgsDb(logger *slog.Logger) *PgsDb { + sb := stub.NewStubDB(logger) + return &PgsDb{ + StubDB: sb, + } +} + +func (p *PgsDb) FindUserForName(name string) (*db.User, error) { + return &db.User{ + ID: testUserID, + Name: testUsername, + }, nil +} + +func (p *PgsDb) FindProjectByName(userID, name string) (*db.Project, error) { + return &db.Project{ + ID: "project-1", + UserID: userID, + Name: name, + ProjectDir: name, + Username: testUsername, + Acl: db.ProjectAcl{ + Type: "public", + }, + }, nil +} + +func mkpath(path string) string { + return fmt.Sprintf("https://%s-test.pgs.test%s", testUsername, path) +} + +func TestApiBasic(t *testing.T) { + bucketName := shared.GetAssetBucketName(testUserID) + cfg := NewConfigSite() + cfg.Domain = "pgs.test" + tt := []*ApiExample{ + { + name: "basic", + path: "/", + want: "hello world!", + status: http.StatusOK, + contentType: "text/html", + + dbpool: NewPgsDb(cfg.Logger), + storage: map[string]map[string]string{ + bucketName: { + "test/index.html": "hello world!", + }, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + request := httptest.NewRequest("GET", mkpath(tc.path), strings.NewReader("")) + responseRecorder := httptest.NewRecorder() + + st, _ := storage.NewStorageMemory(tc.storage) + ch := make(chan *db.AnalyticsVisits) + apiConfig := &shared.ApiConfig{ + Cfg: cfg, + Dbpool: tc.dbpool, + Storage: st, + AnalyticsQueue: ch, + } + handler := shared.CreateServe(mainRoutes, createSubdomainRoutes(publicPerm), apiConfig) + router := http.HandlerFunc(handler) + router(responseRecorder, request) + + if responseRecorder.Code != tc.status { + t.Errorf("Want status '%d', got '%d'", tc.status, responseRecorder.Code) + } + + ct := responseRecorder.Header().Get("content-type") + if ct != tc.contentType { + t.Errorf("Want status '%s', got '%s'", tc.contentType, ct) + } + + body := strings.TrimSpace(responseRecorder.Body.String()) + if body != tc.want { + t.Errorf("Want '%s', got '%s'", tc.want, body) + } + }) + } +} diff --git a/shared/router.go b/shared/router.go index 8a2666cf..4b147d74 100644 --- a/shared/router.go +++ b/shared/router.go @@ -55,7 +55,6 @@ func CreatePProfRoutes(routes []Route) []Route { ) } -type ServeFn func(http.ResponseWriter, *http.Request) type ApiConfig struct { Cfg *ConfigSite Dbpool db.DB @@ -73,7 +72,7 @@ func (hc *ApiConfig) CreateCtx(prevCtx context.Context, subdomain string) contex return ctx } -func CreateServeBasic(routes []Route, ctx context.Context) ServeFn { +func CreateServeBasic(routes []Route, ctx context.Context) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var allow []string for _, route := range routes { @@ -132,7 +131,7 @@ func findRouteConfig(r *http.Request, routes []Route, subdomainRoutes []Route, c return curRoutes, subdomain } -func CreateServe(routes []Route, subdomainRoutes []Route, apiConfig *ApiConfig) ServeFn { +func CreateServe(routes []Route, subdomainRoutes []Route, apiConfig *ApiConfig) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { curRoutes, subdomain := findRouteConfig(r, routes, subdomainRoutes, apiConfig.Cfg) ctx := apiConfig.CreateCtx(r.Context(), subdomain) @@ -143,11 +142,12 @@ func CreateServe(routes []Route, subdomainRoutes []Route, apiConfig *ApiConfig) type ctxDBKey struct{} type ctxStorageKey struct{} -type ctxKey struct{} type ctxLoggerKey struct{} -type ctxSubdomainKey struct{} type ctxCfg struct{} type ctxAnalyticsQueue struct{} + +type ctxSubdomainKey struct{} +type ctxKey struct{} type CtxSshKey struct{} func GetSshCtx(r *http.Request) (ssh.Context, error) { diff --git a/shared/storage/memory.go b/shared/storage/memory.go new file mode 100644 index 00000000..6217677a --- /dev/null +++ b/shared/storage/memory.go @@ -0,0 +1,29 @@ +package storage + +import ( + "io" + + sst "github.com/picosh/pobj/storage" +) + +type StorageMemory struct { + *sst.StorageMemory +} + +func NewStorageMemory(sto map[string]map[string]string) (*StorageMemory, error) { + st, err := sst.NewStorageMemory(sto) + if err != nil { + return nil, err + } + return &StorageMemory{st}, nil +} + +func (s *StorageMemory) ServeObject(bucket sst.Bucket, fpath string, opts *ImgProcessOpts) (io.ReadCloser, string, error) { + obj, _, err := s.GetObject(bucket, fpath) + return obj, GetMimeType(fpath), err +} + +func (s *StorageMemory) GetObjectSize(bucket sst.Bucket, fpath string) (int64, error) { + _, info, err := s.GetObject(bucket, fpath) + return info.Size, err +}