package service
import (
"context"
"errors"
"testing"
"sourcecraft.dev/bigbes/sr-ht-spec/core"
)
var fxProject = core.ProjectRef{Owner: "bigbes", Name: "tarantool"}
func metaRef() core.ProjectRef {
return core.ProjectRef{Owner: "bigbes", Name: core.MetaProjectName}
}
// The meta-project is a value, not a row: resolving it needs no database, which
// is exactly why it needs no sync job either.
func TestEverythingFilterNeedsNoStorage(t *testing.T) {
svc, _ := newService(t) // its database handle cannot be reached
f, err := svc.ResolveProject(context.Background(), metaRef())
if err != nil {
t.Fatalf("ResolveProject(meta): %v", err)
}
if !f.All {
t.Fatal("the meta-project must exclude nothing")
}
// Deliberately unenumerated: a frozen list would omit every space created
// after the resolve.
if len(f.Refs) != 0 || len(f.IDs) != 0 {
t.Fatalf("the meta filter enumerated %d spaces; it must not", len(f.Refs))
}
if f.MatchesNothing() {
t.Fatal("the meta filter matches everything, not nothing")
}
if !f.Matches(fxSpace) || !f.MatchesID(1) {
t.Fatal("the meta filter must match any space")
}
}
// The trap this type exists to prevent: an empty project is not the corpus.
func TestEmptyFilterIsNotEverything(t *testing.T) {
var empty SpaceFilter
if empty.All {
t.Fatal("the zero filter must not be the meta-project")
}
if !empty.MatchesNothing() {
t.Fatal("a filter with no terms selects nothing")
}
if empty.Matches(fxSpace) || empty.MatchesID(1) {
t.Fatal("a filter with no terms must match no space")
}
}
func TestSpaceFilterMatches(t *testing.T) {
other := core.SpaceRef{Owner: "bigbes", Name: "notes"}
f := SpaceFilter{IDs: []int{7}, Refs: []core.SpaceRef{fxSpace}}
if !f.Matches(fxSpace) || !f.MatchesID(7) {
t.Error("a member space must match")
}
if f.Matches(other) || f.MatchesID(8) {
t.Error("a non-member space must not match")
}
if f.MatchesNothing() {
t.Error("a filter with a term does not match nothing")
}
}
func TestProjectLifecycle(t *testing.T) {
svc, _ := newTestService(t)
ctx := context.Background()
p, err := svc.CreateProject(ctx, fxProject)
if err != nil {
t.Fatalf("CreateProject: %v", err)
}
if p.ID == 0 {
t.Error("project has no row id")
}
if _, err := svc.CreateProject(ctx, fxProject); !errors.Is(err, ErrProjectExists) {
t.Errorf("second CreateProject err = %v, want ErrProjectExists", err)
}
got, err := svc.GetProject(ctx, fxProject)
if err != nil {
t.Fatalf("GetProject: %v", err)
}
if got.ID != p.ID || got.Ref != fxProject {
t.Errorf("GetProject = %+v", got)
}
if _, err := svc.GetProject(ctx, core.ProjectRef{Owner: "bigbes", Name: "absent"}); !errors.Is(err, ErrNotFound) {
t.Errorf("GetProject of a missing project = %v, want ErrNotFound", err)
}
projects, err := svc.ListProjects(ctx)
if err != nil {
t.Fatalf("ListProjects: %v", err)
}
if len(projects) != 1 || projects[0].Ref != fxProject {
t.Fatalf("projects = %+v", projects)
}
if err := svc.DeleteProject(ctx, fxProject); err != nil {
t.Fatalf("DeleteProject: %v", err)
}
if err := svc.DeleteProject(ctx, fxProject); !errors.Is(err, ErrNotFound) {
t.Errorf("second DeleteProject = %v, want ErrNotFound", err)
}
}
// The key read: a project is a saved filter over one global index, and this is
// the filter.
func TestResolveProject(t *testing.T) {
svc, _ := newTestService(t)
ctx := context.Background()
rfcs, err := svc.CreateSpace(ctx, fxSpace)
if err != nil {
t.Fatalf("CreateSpace: %v", err)
}
notes, err := svc.CreateSpace(ctx, core.SpaceRef{Owner: "bigbes", Name: "notes"})
if err != nil {
t.Fatalf("CreateSpace: %v", err)
}
if _, err := svc.CreateProject(ctx, fxProject); err != nil {
t.Fatalf("CreateProject: %v", err)
}
// A project with no members selects nothing — never everything.
f, err := svc.ResolveProject(ctx, fxProject)
if err != nil {
t.Fatalf("ResolveProject: %v", err)
}
if f.All {
t.Fatal("an empty project must not resolve to the meta-project")
}
if !f.MatchesNothing() {
t.Fatalf("an empty project resolved to %+v", f)
}
if err := svc.AddSpaceToProject(ctx, fxProject, fxSpace); err != nil {
t.Fatalf("AddSpaceToProject: %v", err)
}
// Membership is a set.
if err := svc.AddSpaceToProject(ctx, fxProject, fxSpace); err != nil {
t.Fatalf("re-adding a member: %v", err)
}
f, err = svc.ResolveProject(ctx, fxProject)
if err != nil {
t.Fatalf("ResolveProject: %v", err)
}
if len(f.Refs) != 1 || f.Refs[0] != fxSpace {
t.Fatalf("filter refs = %+v", f.Refs)
}
if len(f.IDs) != 1 || f.IDs[0] != rfcs.ID {
t.Fatalf("filter ids = %+v, want [%d]", f.IDs, rfcs.ID)
}
if !f.Matches(fxSpace) || !f.MatchesID(rfcs.ID) {
t.Error("the member space must match")
}
if f.Matches(notes.Ref) || f.MatchesID(notes.ID) {
t.Error("a space outside the project must not match")
}
// Membership listing, the counterpart of the filter.
spaces, err := svc.ProjectSpaces(ctx, fxProject)
if err != nil {
t.Fatalf("ProjectSpaces: %v", err)
}
if len(spaces) != 1 || spaces[0].Ref != fxSpace {
t.Fatalf("ProjectSpaces = %+v", spaces)
}
if spaces[0].Repo != nil {
t.Error("ProjectSpaces opened repositories; listing must stay cheap")
}
if err := svc.RemoveSpaceFromProject(ctx, fxProject, fxSpace); err != nil {
t.Fatalf("RemoveSpaceFromProject: %v", err)
}
if err := svc.RemoveSpaceFromProject(ctx, fxProject, fxSpace); !errors.Is(err, ErrNotFound) {
t.Errorf("removing a non-member = %v, want ErrNotFound", err)
}
// The space survives its removal from a filter.
if _, err := svc.OpenSpace(ctx, fxSpace); err != nil {
t.Errorf("removing a space from a project broke the space: %v", err)
}
if _, err := svc.ResolveProject(ctx, core.ProjectRef{Owner: "bigbes", Name: "absent"}); !errors.Is(err, ErrNotFound) {
t.Errorf("ResolveProject of a missing project = %v, want ErrNotFound", err)
}
}
// The meta-project unifies what the service owns, with no membership to
// maintain: a space created after it was last looked at is in it already.
func TestMetaProjectListsEverySpaceIncludingNewOnes(t *testing.T) {
svc, _ := newTestService(t)
ctx := context.Background()
if _, err := svc.CreateSpace(ctx, fxSpace); err != nil {
t.Fatalf("CreateSpace: %v", err)
}
spaces, err := svc.ProjectSpaces(ctx, metaRef())
if err != nil {
t.Fatalf("ProjectSpaces(meta): %v", err)
}
if len(spaces) != 1 {
t.Fatalf("meta-project lists %d spaces, want 1", len(spaces))
}
if _, err := svc.CreateSpace(ctx, core.SpaceRef{Owner: "bigbes", Name: "notes"}); err != nil {
t.Fatalf("CreateSpace: %v", err)
}
spaces, err = svc.ProjectSpaces(ctx, metaRef())
if err != nil {
t.Fatalf("ProjectSpaces(meta): %v", err)
}
if len(spaces) != 2 {
t.Fatalf("meta-project lists %d spaces after a new one was created, want 2", len(spaces))
}
}
// The meta-project has no row, so every operation on a row refuses it — and the
// one that creates rows refuses to make one that would shadow it.
func TestMetaProjectHasNoRow(t *testing.T) {
svc, _ := newTestService(t)
ctx := context.Background()
if _, err := svc.CreateSpace(ctx, fxSpace); err != nil {
t.Fatalf("CreateSpace: %v", err)
}
if _, err := svc.CreateProject(ctx, metaRef()); !errors.Is(err, core.ErrReservedName) {
t.Errorf("CreateProject(meta) = %v, want ErrReservedName", err)
}
if _, err := svc.GetProject(ctx, metaRef()); !errors.Is(err, ErrNotFound) {
t.Errorf("GetProject(meta) = %v, want ErrNotFound", err)
}
if err := svc.DeleteProject(ctx, metaRef()); !errors.Is(err, ErrNotFound) {
t.Errorf("DeleteProject(meta) = %v, want ErrNotFound", err)
}
if err := svc.AddSpaceToProject(ctx, metaRef(), fxSpace); !errors.Is(err, ErrNotFound) {
t.Errorf("AddSpaceToProject(meta) = %v, want ErrNotFound", err)
}
if err := svc.RemoveSpaceFromProject(ctx, metaRef(), fxSpace); !errors.Is(err, ErrNotFound) {
t.Errorf("RemoveSpaceFromProject(meta) = %v, want ErrNotFound", err)
}
// And it is not in the project list, because the list is of rows.
projects, err := svc.ListProjects(ctx)
if err != nil {
t.Fatalf("ListProjects: %v", err)
}
if len(projects) != 0 {
t.Fatalf("ListProjects = %+v, want none", projects)
}
}
func TestProjectMembershipRequiresBothSides(t *testing.T) {
svc, _ := newTestService(t)
ctx := context.Background()
if _, err := svc.CreateProject(ctx, fxProject); err != nil {
t.Fatalf("CreateProject: %v", err)
}
if err := svc.AddSpaceToProject(ctx, fxProject, fxSpace); !errors.Is(err, ErrNotFound) {
t.Errorf("adding a space that does not exist = %v, want ErrNotFound", err)
}
if _, err := svc.CreateSpace(ctx, fxSpace); err != nil {
t.Fatalf("CreateSpace: %v", err)
}
missing := core.ProjectRef{Owner: "bigbes", Name: "absent"}
if err := svc.AddSpaceToProject(ctx, missing, fxSpace); !errors.Is(err, ErrNotFound) {
t.Errorf("adding to a project that does not exist = %v, want ErrNotFound", err)
}
}
// A space belongs to any number of projects; a project is a filter, not an
// owner, so nothing about the space changes when one contains it.
func TestProjectsForSpace(t *testing.T) {
svc, _ := newTestService(t)
ctx := context.Background()
if _, err := svc.CreateSpace(ctx, fxSpace); err != nil {
t.Fatalf("CreateSpace: %v", err)
}
projects, err := svc.ProjectsForSpace(ctx, fxSpace)
if err != nil {
t.Fatalf("ProjectsForSpace: %v", err)
}
if len(projects) != 0 {
t.Fatalf("a space in no project reports %+v", projects)
}
for _, name := range []string{"tarantool", "all-docs"} {
ref := core.ProjectRef{Owner: "bigbes", Name: name}
if _, err := svc.CreateProject(ctx, ref); err != nil {
t.Fatalf("CreateProject %s: %v", ref, err)
}
if err := svc.AddSpaceToProject(ctx, ref, fxSpace); err != nil {
t.Fatalf("AddSpaceToProject %s: %v", ref, err)
}
}
projects, err = svc.ProjectsForSpace(ctx, fxSpace)
if err != nil {
t.Fatalf("ProjectsForSpace: %v", err)
}
if len(projects) != 2 {
t.Fatalf("projects = %+v, want 2", projects)
}
if projects[0].Ref.Name != "all-docs" || projects[1].Ref.Name != "tarantool" {
t.Errorf("projects are not ordered by name: %+v", projects)
}
if _, err := svc.ProjectsForSpace(ctx, core.SpaceRef{Owner: "bigbes", Name: "absent"}); !errors.Is(err, ErrNotFound) {
t.Errorf("ProjectsForSpace of a missing space = %v, want ErrNotFound", err)
}
}