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.Everything() { 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) { empty := core.SpacesFilter(nil, nil) if empty.Everything() { t.Fatal("a filter over no spaces 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") } // And the state that is neither answer: nobody said. It is not silently // one of the two — a query refuses it, which is what makes "I forgot to // set the scope" a failure instead of a wrong-scoped answer. var unset SpaceFilter if !unset.IsZero() || unset.Everything() { t.Fatal("the zero filter must be neither everything nor a set scope") } } func TestSpaceFilterMatches(t *testing.T) { other := core.SpaceRef{Owner: "bigbes", Name: "notes"} f := core.SpacesFilter([]core.SpaceRef{fxSpace}, []int{7}) 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.Everything() { t.Fatal("an empty project must not resolve to the meta-project") } if f.IsZero() { t.Fatal("a resolved project must carry a scope, not the zero filter") } 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) } }