mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
147 lines
5.1 KiB
Go
147 lines
5.1 KiB
Go
package webapp
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"errors"
|
||
|
|
"path/filepath"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Every Directory implementation has to behave the same way from the hub's
|
||
|
|
// side, whether it owns its organizations or mirrors someone else's. This is
|
||
|
|
// the contract, run against each implementation — the same shape as
|
||
|
|
// db_conformance_test does for MetaStore backends.
|
||
|
|
//
|
||
|
|
// A read-only directory (one whose orgs live in an external identity system)
|
||
|
|
// is a first-class case: it answers reads normally and refuses writes with
|
||
|
|
// ErrManagedElsewhere, which is what lets the hub answer 409 without knowing
|
||
|
|
// anything about the system on the other side.
|
||
|
|
|
||
|
|
// readOnlyDir wraps a directory and refuses every write, the way an
|
||
|
|
// implementation backed by an external identity system does.
|
||
|
|
type readOnlyDir struct{ Directory }
|
||
|
|
|
||
|
|
func (readOnlyDir) Create(string, string) (Org, error) { return Org{}, ErrManagedElsewhere }
|
||
|
|
func (readOnlyDir) Rename(string, string) error { return ErrManagedElsewhere }
|
||
|
|
func (readOnlyDir) AddMember(string, string, string) error { return ErrManagedElsewhere }
|
||
|
|
func (readOnlyDir) SetRole(string, string, string) error { return ErrManagedElsewhere }
|
||
|
|
func (readOnlyDir) RemoveMember(string, string) error { return ErrManagedElsewhere }
|
||
|
|
func (readOnlyDir) CreateInvite(string, string, time.Duration) (OrgInvite, error) {
|
||
|
|
return OrgInvite{}, ErrManagedElsewhere
|
||
|
|
}
|
||
|
|
func (readOnlyDir) ManageURL(orgID string) string { return "https://elsewhere.example/" + orgID }
|
||
|
|
|
||
|
|
func TestDirectoryConformance(t *testing.T) {
|
||
|
|
t.Run("LocalDirectory", func(t *testing.T) {
|
||
|
|
db, err := OpenOrgDB(filepath.Join(t.TempDir(), "orgs.json"))
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
dirReads(t, LocalDirectory{OrgDB: db}, true)
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("read-only directory", func(t *testing.T) {
|
||
|
|
db, err := OpenOrgDB(filepath.Join(t.TempDir(), "orgs.json"))
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
// Seed through the underlying store: an external directory's contents
|
||
|
|
// arrive by mirroring, not through the hub's write path.
|
||
|
|
o, err := db.Create("acme", "alice@x.io")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
dir := readOnlyDir{Directory: LocalDirectory{OrgDB: db}}
|
||
|
|
dirReads(t, dir, false)
|
||
|
|
|
||
|
|
// Reads still work on the seeded org...
|
||
|
|
if got := dir.Role(o.ID, "alice@x.io"); got != RoleOwner {
|
||
|
|
t.Errorf("Role = %q, want owner", got)
|
||
|
|
}
|
||
|
|
// ...and every write is refused the same recognizable way.
|
||
|
|
for name, err := range map[string]error{
|
||
|
|
"Rename": dir.Rename(o.ID, "nope"),
|
||
|
|
"AddMember": dir.AddMember(o.ID, "bob@x.io", RoleMember),
|
||
|
|
"SetRole": dir.SetRole(o.ID, "alice@x.io", RoleMember),
|
||
|
|
"RemoveMember": dir.RemoveMember(o.ID, "alice@x.io"),
|
||
|
|
} {
|
||
|
|
if !errors.Is(err, ErrManagedElsewhere) {
|
||
|
|
t.Errorf("%s err = %v, want ErrManagedElsewhere", name, err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if _, err := dir.Create("other", "alice@x.io"); !errors.Is(err, ErrManagedElsewhere) {
|
||
|
|
t.Errorf("Create err = %v, want ErrManagedElsewhere", err)
|
||
|
|
}
|
||
|
|
if _, err := dir.CreateInvite(o.ID, "alice@x.io", 0); !errors.Is(err, ErrManagedElsewhere) {
|
||
|
|
t.Errorf("CreateInvite err = %v, want ErrManagedElsewhere", err)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
// dirReads exercises the read surface every implementation must answer, and
|
||
|
|
// the writes only an owning directory supports.
|
||
|
|
func dirReads(t *testing.T, dir Directory, writable bool) {
|
||
|
|
t.Helper()
|
||
|
|
|
||
|
|
if writable {
|
||
|
|
o, err := dir.Create("acme", "alice@x.io")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if got, ok := dir.Get(o.ID); !ok || got.Name != "acme" {
|
||
|
|
t.Fatalf("Get = %+v ok=%v", got, ok)
|
||
|
|
}
|
||
|
|
if got := dir.Role(o.ID, "ALICE@x.io"); got != RoleOwner {
|
||
|
|
t.Errorf("Role is not email-normalized: %q", got)
|
||
|
|
}
|
||
|
|
if err := dir.AddMember(o.ID, "bob@x.io", RoleMember); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
// The invariants live in the implementation, not in the caller.
|
||
|
|
if err := dir.RemoveMember(o.ID, "alice@x.io"); err == nil {
|
||
|
|
t.Error("removing the last owner must be refused")
|
||
|
|
}
|
||
|
|
if err := dir.SetRole(o.ID, "alice@x.io", RoleMember); err == nil {
|
||
|
|
t.Error("demoting the last owner must be refused")
|
||
|
|
}
|
||
|
|
inv, err := dir.CreateInvite(o.ID, "alice@x.io", time.Hour)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if !dir.ValidInvite(inv.Token) {
|
||
|
|
t.Error("fresh invite is not valid")
|
||
|
|
}
|
||
|
|
if got := dir.ListInvites(o.ID); len(got) != 1 {
|
||
|
|
t.Errorf("ListInvites = %d, want 1", len(got))
|
||
|
|
}
|
||
|
|
if _, ok := dir.Redeem(inv.Token); !ok {
|
||
|
|
t.Error("Redeem failed")
|
||
|
|
}
|
||
|
|
dir.RecordInviteUse(inv.Token)
|
||
|
|
if !dir.RevokeInvite(inv.Token) {
|
||
|
|
t.Error("RevokeInvite failed")
|
||
|
|
}
|
||
|
|
if dir.ValidInvite(inv.Token) {
|
||
|
|
t.Error("revoked invite is still valid")
|
||
|
|
}
|
||
|
|
if len(dir.OrgsFor("bob@x.io")) != 1 {
|
||
|
|
t.Error("OrgsFor missed the org bob belongs to")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Unknown ids are answered, never panicked on — Role in particular runs on
|
||
|
|
// every project request including device sync.
|
||
|
|
if got := dir.Role("o-nope", "nobody@x.io"); got != "" {
|
||
|
|
t.Errorf("Role of a non-member = %q, want \"\"", got)
|
||
|
|
}
|
||
|
|
if _, ok := dir.Get("o-nope"); ok {
|
||
|
|
t.Error("Get of an unknown org reported ok")
|
||
|
|
}
|
||
|
|
if dir.ValidInvite("not-a-token") {
|
||
|
|
t.Error("bogus invite reported valid")
|
||
|
|
}
|
||
|
|
if dir.ManageURL("o-1234") == "" {
|
||
|
|
t.Error("ManageURL must always give the client somewhere to go")
|
||
|
|
}
|
||
|
|
}
|