This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package authsqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gamertan.com/web/auth"
|
||||
"gamertan.com/web/organizations"
|
||||
)
|
||||
|
||||
func TestInstanceDirectoriesAreBoundedCredentialFreeAndIndependentOfMembership(t *testing.T) {
|
||||
store, err := Open(filepath.Join(t.TempDir(), "directory.sqlite"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer store.Close()
|
||||
ctx := t.Context()
|
||||
for index := 0; index < 205; index++ {
|
||||
id := fmt.Sprintf("record-%03d", index)
|
||||
status := []string{"active", "suspended", "disabled"}[index%3]
|
||||
_, err = store.db.ExecContext(ctx, `INSERT INTO gwf_users(id,username,username_normalized,email,email_normalized,display_name,status,password_change_required,registration_pending,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,1,1)`, id, id, id, id+"@example.test", id+"@example.test", "Person "+id, status, index%2, index%5 == 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = store.db.ExecContext(ctx, `INSERT INTO gwf_organizations(id,slug,name,personal,status,revision,created_at,updated_at) VALUES(?,?,?,?,?,1,1,1)`, id, id, "Business "+id, index%2, []string{"active", "archived"}[index%2])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
users, err := store.UserDirectory(ctx, auth.UserDirectoryQuery{})
|
||||
if err != nil || len(users.Users) != 50 || users.NextID != "record-049" {
|
||||
t.Fatalf("default users: %d %q %v", len(users.Users), users.NextID, err)
|
||||
}
|
||||
if !users.Users[0].RegistrationPending || users.Users[1].Status != "suspended" || !users.Users[1].PasswordChangeRequired || users.Users[2].Status != "disabled" {
|
||||
t.Fatal("administrative account states were hidden")
|
||||
}
|
||||
encoded, _ := json.Marshal(users)
|
||||
for _, secret := range []string{"password_hash", "Session", "Digest", "Credential", "Recovery"} {
|
||||
if strings.Contains(string(encoded), secret) {
|
||||
t.Fatalf("directory leaked credential field %s", secret)
|
||||
}
|
||||
}
|
||||
for _, size := range []int{1, 50, 200} {
|
||||
userAfter, orgAfter, count := "", "", 0
|
||||
for {
|
||||
users, err := store.UserDirectory(ctx, auth.UserDirectoryQuery{AfterID: userAfter, Limit: size})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
orgs, err := store.OrganizationDirectory(ctx, organizations.DirectoryQuery{AfterID: orgAfter, Limit: size})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(users.Users) != len(orgs.Organizations) || len(users.Users) > size {
|
||||
t.Fatal("invalid page bound")
|
||||
}
|
||||
for index, user := range users.Users {
|
||||
want := fmt.Sprintf("record-%03d", count)
|
||||
if user.ID != want || orgs.Organizations[index].ID != want {
|
||||
t.Fatalf("pagination skipped/duplicated %s", want)
|
||||
}
|
||||
count++
|
||||
}
|
||||
if users.NextID == "" || orgs.NextID == "" {
|
||||
if users.NextID != orgs.NextID || count != 205 {
|
||||
t.Fatalf("early end: %d", count)
|
||||
}
|
||||
break
|
||||
}
|
||||
userAfter, orgAfter = users.NextID, orgs.NextID
|
||||
}
|
||||
}
|
||||
orgs, err := store.OrganizationDirectory(ctx, organizations.DirectoryQuery{Limit: 2})
|
||||
if err != nil || orgs.Organizations[0].Personal || !orgs.Organizations[1].Personal || orgs.Organizations[1].Status != "archived" {
|
||||
t.Fatal("personal/archived organizations omitted")
|
||||
}
|
||||
// A display-name change cannot move a record behind a stable-ID cursor.
|
||||
if _, err = store.db.Exec(`UPDATE gwf_users SET display_name='AAA' WHERE id='record-050'`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
next, err := store.UserDirectory(ctx, auth.UserDirectoryQuery{AfterID: "record-049", Limit: 1})
|
||||
if err != nil || next.Users[0].ID != "record-050" {
|
||||
t.Fatal("name change disturbed cursor")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstanceDirectoryLiteralSearchValidationAndCancellation(t *testing.T) {
|
||||
store, err := Open(filepath.Join(t.TempDir(), "directory.sqlite"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer store.Close()
|
||||
_, err = store.db.Exec(`INSERT INTO gwf_users(id,username,username_normalized,email,email_normalized,display_name,status,created_at,updated_at) VALUES('person-001','Alice','alice','Alice@example.test','alice@example.test','Élodie 50%_\ works','active',1,1)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = store.db.Exec(`INSERT INTO gwf_organizations(id,slug,name,personal,status,revision,created_at,updated_at) VALUES('company-001','alice-company','Élodie 50%_\ works',0,'active',1,1,1)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, search := range []string{"", " ALICE ", "example.test", "person-001", "Élodie", `50%_\`} {
|
||||
page, err := store.UserDirectory(t.Context(), auth.UserDirectoryQuery{Search: search})
|
||||
if err != nil || len(page.Users) != 1 || page.NextID != "" {
|
||||
t.Errorf("user literal search %q: %#v %v", search, page, err)
|
||||
}
|
||||
}
|
||||
for _, search := range []string{"", "ALICE", "company-001", "Élodie", `50%_\`} {
|
||||
page, err := store.OrganizationDirectory(t.Context(), organizations.DirectoryQuery{Search: search})
|
||||
if err != nil || len(page.Organizations) != 1 || page.NextID != "" {
|
||||
t.Errorf("organization literal search %q: %#v %v", search, page, err)
|
||||
}
|
||||
}
|
||||
for _, search := range []string{"absent", "%' OR 1=1 --", "%_%", "\\_%"} {
|
||||
users, err := store.UserDirectory(t.Context(), auth.UserDirectoryQuery{Search: search})
|
||||
if err != nil || users.Users == nil || len(users.Users) != 0 {
|
||||
t.Errorf("nonliteral user search %q", search)
|
||||
}
|
||||
orgs, err := store.OrganizationDirectory(t.Context(), organizations.DirectoryQuery{Search: search})
|
||||
if err != nil || orgs.Organizations == nil || len(orgs.Organizations) != 0 {
|
||||
t.Errorf("nonliteral org search %q", search)
|
||||
}
|
||||
}
|
||||
for _, query := range []auth.UserDirectoryQuery{{Search: strings.Repeat("a", 129)}, {Search: "bad\x00value"}, {Search: "bad\nvalue"}, {Search: "\xff"}, {AfterID: "bad/id"}, {AfterID: strings.Repeat("a", 129)}, {Limit: -1}, {Limit: 201}} {
|
||||
if _, err := store.UserDirectory(t.Context(), query); !errors.Is(err, auth.ErrDirectoryQuery) {
|
||||
t.Errorf("invalid user query accepted: %#v %v", query, err)
|
||||
}
|
||||
if _, err := store.OrganizationDirectory(t.Context(), organizations.DirectoryQuery{Search: query.Search, AfterID: query.AfterID, Limit: query.Limit}); !errors.Is(err, organizations.ErrDirectoryQuery) {
|
||||
t.Errorf("invalid org query accepted: %#v %v", query, err)
|
||||
}
|
||||
}
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
if _, err = store.UserDirectory(ctx, auth.UserDirectoryQuery{}); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("user cancellation: %v", err)
|
||||
}
|
||||
if _, err = store.OrganizationDirectory(ctx, organizations.DirectoryQuery{}); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("organization cancellation: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user