514 lines
13 KiB
Go
514 lines
13 KiB
Go
package store
|
|
|
|
import (
|
|
"errors"
|
|
"reflect"
|
|
"strconv"
|
|
"sync"
|
|
"testing"
|
|
|
|
"helix-proxy/internal/config"
|
|
)
|
|
|
|
// --- test helpers ---
|
|
|
|
func newTestBbolt(t *testing.T) Store {
|
|
t.Helper()
|
|
tmp := t.TempDir()
|
|
config.ResetForTest()
|
|
t.Setenv("DATA_DIR", tmp)
|
|
s, err := NewBboltStore()
|
|
if err != nil {
|
|
t.Fatalf("NewBboltStore: %v", err)
|
|
}
|
|
return s
|
|
}
|
|
|
|
// closeIfBbolt helps cleanup for bbolt (interface doesn't have Close but impl does)
|
|
func closeIfBbolt(s Store) {
|
|
if c, ok := s.(interface{ Close() error }); ok {
|
|
c.Close()
|
|
}
|
|
}
|
|
|
|
func TestNew_SeedsAdminAndSettings(t *testing.T) {
|
|
s := newTestBbolt(t)
|
|
defer closeIfBbolt(s)
|
|
|
|
if !s.IsSetup() {
|
|
t.Error("IsSetup false after new")
|
|
}
|
|
users := s.GetUsers()
|
|
if len(users) != 1 || users[0].Email != "admin@example.com" || users[0].ID != 1 {
|
|
t.Errorf("seeded admin wrong: %+v", users)
|
|
}
|
|
settings := s.GetSettings()
|
|
if settings["default_site"] != "congratulations" {
|
|
t.Errorf("default settings wrong: %v", settings)
|
|
}
|
|
}
|
|
|
|
func TestIDGeneration(t *testing.T) {
|
|
s := newTestBbolt(t)
|
|
defer closeIfBbolt(s)
|
|
|
|
h1, _ := s.CreateProxyHost(ProxyHost{DomainNames: []string{"a.com"}})
|
|
h2, _ := s.CreateProxyHost(ProxyHost{DomainNames: []string{"b.com"}})
|
|
if h1.ID == 0 || h2.ID == 0 || h2.ID <= h1.ID {
|
|
t.Errorf("ids not increasing: %d %d", h1.ID, h2.ID)
|
|
}
|
|
if h1.CreatedOn == "" {
|
|
t.Error("CreateProxyHost did not populate CreatedOn")
|
|
}
|
|
al1, _ := s.CreateAccessList(AccessList{Name: "t1"})
|
|
if al1.CreatedOn == "" {
|
|
t.Error("CreateAccessList did not populate CreatedOn")
|
|
}
|
|
st1, _ := s.CreateStream(Stream{IncomingPort: 1})
|
|
if st1.CreatedOn == "" {
|
|
t.Error("CreateStream did not populate CreatedOn")
|
|
}
|
|
// explicit id should be honored? (current impls assign only if 0)
|
|
h3, _ := s.CreateProxyHost(ProxyHost{ID: 99, DomainNames: []string{"c.com"}})
|
|
if h3.ID != 99 {
|
|
t.Errorf("explicit id not kept: %d", h3.ID)
|
|
}
|
|
}
|
|
|
|
func TestProxyHostCRUD(t *testing.T) {
|
|
s := newTestBbolt(t)
|
|
defer closeIfBbolt(s)
|
|
|
|
// create
|
|
h := ProxyHost{
|
|
DomainNames: []string{"example.com", "www.example.com"},
|
|
ForwardScheme: "https",
|
|
ForwardHost: "10.0.0.1",
|
|
ForwardPort: 8443,
|
|
Enabled: true,
|
|
Meta: map[string]any{"foo": "bar"},
|
|
}
|
|
created, err := s.CreateProxyHost(h)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if created.ID == 0 {
|
|
t.Fatal("no id assigned")
|
|
}
|
|
if !reflect.DeepEqual(created.DomainNames, h.DomainNames) || created.Meta["foo"] != "bar" {
|
|
t.Error("roundtrip fidelity lost on create")
|
|
}
|
|
|
|
// get list / get
|
|
list := s.GetProxyHosts()
|
|
if len(list) != 1 {
|
|
t.Errorf("list len %d", len(list))
|
|
}
|
|
got, ok := s.GetProxyHost(created.ID)
|
|
if !ok || got.ID != created.ID {
|
|
t.Error("get by id failed")
|
|
}
|
|
|
|
// update
|
|
created.ForwardHost = "10.0.0.2"
|
|
created.Meta["new"] = 42
|
|
up, err := s.UpdateProxyHost(created)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if up.ForwardHost != "10.0.0.2" || up.Meta["new"] != 42 {
|
|
t.Error("update not persisted")
|
|
}
|
|
if up.CreatedOn == "" {
|
|
t.Error("UpdateProxyHost did not preserve CreatedOn")
|
|
}
|
|
|
|
// set enabled
|
|
if err := s.SetProxyHostEnabled(created.ID, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
g2, _ := s.GetProxyHost(created.ID)
|
|
if g2.Enabled {
|
|
t.Error("set enabled false failed")
|
|
}
|
|
|
|
// delete
|
|
if err := s.DeleteProxyHost(created.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := s.GetProxyHost(created.ID); ok {
|
|
t.Error("delete did not remove")
|
|
}
|
|
}
|
|
|
|
func TestProxyHostDuplicateSources(t *testing.T) {
|
|
s := newTestBbolt(t)
|
|
defer closeIfBbolt(s)
|
|
|
|
h1, err := s.CreateProxyHost(ProxyHost{DomainNames: []string{"a.example.com"}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = s.CreateProxyHost(ProxyHost{DomainNames: []string{"a.example.com"}})
|
|
if !errors.Is(err, ErrDuplicateProxySource) {
|
|
t.Fatalf("expected duplicate error on cross-host conflict, got %v", err)
|
|
}
|
|
|
|
_, err = s.CreateProxyHost(ProxyHost{DomainNames: []string{"A.EXAMPLE.COM"}})
|
|
if !errors.Is(err, ErrDuplicateProxySource) {
|
|
t.Fatalf("expected duplicate error on case-insensitive conflict, got %v", err)
|
|
}
|
|
|
|
_, err = s.CreateProxyHost(ProxyHost{DomainNames: []string{"b.example.com", "b.example.com"}})
|
|
if !errors.Is(err, ErrDuplicateProxySource) {
|
|
t.Fatalf("expected duplicate error within same host, got %v", err)
|
|
}
|
|
|
|
h1.DomainNames = []string{"a.example.com", "c.example.com"}
|
|
if _, err = s.UpdateProxyHost(h1); err != nil {
|
|
t.Fatalf("update own domains: %v", err)
|
|
}
|
|
|
|
h2, err := s.CreateProxyHost(ProxyHost{DomainNames: []string{"d.example.com"}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
h2.DomainNames = []string{"c.example.com"}
|
|
_, err = s.UpdateProxyHost(h2)
|
|
if !errors.Is(err, ErrDuplicateProxySource) {
|
|
t.Fatalf("expected duplicate error on update conflict, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRedirectionHostDuplicateSources(t *testing.T) {
|
|
s := newTestBbolt(t)
|
|
defer closeIfBbolt(s)
|
|
|
|
_, err := s.CreateRedirectionHost(RedirectionHost{DomainNames: []string{"redir.example.com"}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err = s.CreateRedirectionHost(RedirectionHost{DomainNames: []string{"redir.example.com"}})
|
|
if !errors.Is(err, ErrDuplicateRedirectionSource) {
|
|
t.Fatalf("expected duplicate error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDeadHostDuplicateSources(t *testing.T) {
|
|
s := newTestBbolt(t)
|
|
defer closeIfBbolt(s)
|
|
|
|
_, err := s.CreateDeadHost(DeadHost{DomainNames: []string{"dead.example.com"}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err = s.CreateDeadHost(DeadHost{DomainNames: []string{"DEAD.example.com"}})
|
|
if !errors.Is(err, ErrDuplicateDeadSource) {
|
|
t.Fatalf("expected duplicate error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestStreamDuplicateIncomingPort(t *testing.T) {
|
|
s := newTestBbolt(t)
|
|
defer closeIfBbolt(s)
|
|
|
|
_, err := s.CreateStream(Stream{IncomingPort: 9001, ForwardingHost: "127.0.0.1", ForwardingPort: 80})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err = s.CreateStream(Stream{IncomingPort: 9001, ForwardingHost: "127.0.0.1", ForwardingPort: 8080})
|
|
if !errors.Is(err, ErrDuplicateStreamPort) {
|
|
t.Fatalf("expected duplicate port error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestStreamUpdateDuplicateIncomingPort(t *testing.T) {
|
|
s := newTestBbolt(t)
|
|
defer closeIfBbolt(s)
|
|
|
|
a, err := s.CreateStream(Stream{IncomingPort: 9001, ForwardingHost: "127.0.0.1", ForwardingPort: 80})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, err := s.CreateStream(Stream{IncomingPort: 9002, ForwardingHost: "127.0.0.1", ForwardingPort: 80})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b.IncomingPort = 9001
|
|
_, err = s.UpdateStream(b)
|
|
if !errors.Is(err, ErrDuplicateStreamPort) {
|
|
t.Fatalf("expected duplicate port error on update, got %v", err)
|
|
}
|
|
_ = a
|
|
}
|
|
|
|
func TestAccessListCRUD(t *testing.T) {
|
|
s := newTestBbolt(t)
|
|
defer closeIfBbolt(s)
|
|
|
|
al := AccessList{
|
|
Name: "test al",
|
|
SatisfyAny: true,
|
|
Clients: []AccessClient{{Address: "1.2.3.4", Directive: "allow"}},
|
|
Items: []AccessItem{{Username: "u", Password: "p"}},
|
|
PassAuth: true,
|
|
}
|
|
created, err := s.CreateAccessList(al)
|
|
if err != nil || created.ID == 0 {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
if len(created.Clients) != 1 || len(created.Items) != 1 {
|
|
t.Error("clients/items nilled?")
|
|
}
|
|
|
|
// update
|
|
created.Name = "renamed"
|
|
up, err := s.UpdateAccessList(created)
|
|
if err != nil || up.Name != "renamed" {
|
|
t.Fatalf("update: %v", err)
|
|
}
|
|
if up.CreatedOn == "" {
|
|
t.Error("UpdateAccessList did not preserve CreatedOn")
|
|
}
|
|
|
|
if err := s.DeleteAccessList(created.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := s.GetAccessList(created.ID); ok {
|
|
t.Error("not deleted")
|
|
}
|
|
}
|
|
|
|
func TestStreamCRUD(t *testing.T) {
|
|
s := newTestBbolt(t)
|
|
defer closeIfBbolt(s)
|
|
|
|
st := Stream{
|
|
IncomingPort: 1234,
|
|
ForwardingHost: "127.0.0.1",
|
|
ForwardingPort: 5678,
|
|
TCPForwarding: true,
|
|
Enabled: true,
|
|
Meta: map[string]any{"k": 1},
|
|
}
|
|
c, err := s.CreateStream(st)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.ID == 0 || c.Meta == nil {
|
|
t.Error("id or meta")
|
|
}
|
|
c.Enabled = false
|
|
up, _ := s.UpdateStream(c)
|
|
if up.CreatedOn == "" {
|
|
t.Error("UpdateStream did not preserve CreatedOn")
|
|
}
|
|
if err := s.SetStreamEnabled(c.ID, true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
g, _ := s.GetStream(c.ID)
|
|
if !g.Enabled {
|
|
t.Error("set enabled")
|
|
}
|
|
_ = s.DeleteStream(c.ID)
|
|
}
|
|
|
|
func TestCertificateCRUD(t *testing.T) {
|
|
s := newTestBbolt(t)
|
|
defer closeIfBbolt(s)
|
|
|
|
c := Certificate{
|
|
Provider: "letsencrypt",
|
|
DomainNames: []string{"ex.com"},
|
|
Meta: map[string]any{"cert": "pem"},
|
|
}
|
|
created, err := s.CreateCertificate(c)
|
|
if err != nil || len(created.DomainNames) == 0 || created.Meta == nil {
|
|
t.Fatal(err)
|
|
}
|
|
created.ExpiresOn = "2099-01-01"
|
|
origCreated := created.CreatedOn
|
|
if err := s.SetCertificate(created); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
g, _ := s.GetCertificate(created.ID)
|
|
if g.ExpiresOn != "2099-01-01" {
|
|
t.Error("set cert")
|
|
}
|
|
if g.CreatedOn != origCreated {
|
|
t.Error("SetCertificate did not preserve CreatedOn")
|
|
}
|
|
_ = s.DeleteCertificate(created.ID)
|
|
}
|
|
|
|
func TestRedirectionDeadHostCRUD(t *testing.T) {
|
|
s := newTestBbolt(t)
|
|
defer closeIfBbolt(s)
|
|
|
|
rh := RedirectionHost{DomainNames: []string{"r.com"}, ForwardDomainName: "t.com", Enabled: true, Meta: map[string]any{}}
|
|
crh, _ := s.CreateRedirectionHost(rh)
|
|
crh.ForwardHTTPCode = 301
|
|
rup, _ := s.UpdateRedirectionHost(crh)
|
|
if rup.CreatedOn == "" {
|
|
t.Error("UpdateRedirectionHost did not preserve CreatedOn")
|
|
}
|
|
_ = s.SetRedirectionHostEnabled(crh.ID, false)
|
|
_ = s.DeleteRedirectionHost(crh.ID)
|
|
|
|
dh := DeadHost{DomainNames: []string{"d.com"}, Enabled: true, Meta: map[string]any{}}
|
|
cdh, _ := s.CreateDeadHost(dh)
|
|
dup, _ := s.UpdateDeadHost(cdh)
|
|
if dup.CreatedOn == "" {
|
|
t.Error("UpdateDeadHost did not preserve CreatedOn")
|
|
}
|
|
_ = s.SetDeadHostEnabled(cdh.ID, false)
|
|
_ = s.DeleteDeadHost(cdh.ID)
|
|
}
|
|
|
|
func TestSettingsAudit(t *testing.T) {
|
|
s := newTestBbolt(t)
|
|
defer closeIfBbolt(s)
|
|
|
|
if err := s.SetSetting("foo", "bar"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m := s.GetSettings()
|
|
if m["foo"] != "bar" {
|
|
t.Errorf("set/get setting: %v", m)
|
|
}
|
|
|
|
log := AuditLog{UserID: 1, ObjectType: "test", ObjectID: 99, Action: "create", Meta: map[string]any{"x": 1}}
|
|
added, err := s.AddAuditLog(log)
|
|
if err != nil || added.ID == 0 {
|
|
t.Fatalf("add audit: %v", err)
|
|
}
|
|
if added.CreatedOn == "" {
|
|
t.Error("AddAuditLog did not populate CreatedOn")
|
|
}
|
|
logs := s.GetAuditLogs()
|
|
if len(logs) < 1 || logs[len(logs)-1].CreatedOn == "" {
|
|
t.Error("no audit logs or missing CreatedOn")
|
|
}
|
|
}
|
|
|
|
func TestUsersCRUD(t *testing.T) {
|
|
s := newTestBbolt(t)
|
|
defer closeIfBbolt(s)
|
|
|
|
u := User{Email: "new@example.com", Name: "New", Roles: []string{"user"}, Password: "pw"}
|
|
created, err := s.CreateUser(u)
|
|
if err != nil || created.ID == 0 {
|
|
t.Fatal(err)
|
|
}
|
|
created.Name = "Renamed"
|
|
if err := s.UpdateUser(created); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.UpdateUserFull(created); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
by, ok := s.GetUserByEmail("new@example.com")
|
|
if !ok || by.Name != "Renamed" {
|
|
t.Error("get by email or update")
|
|
}
|
|
if err := s.DeleteUser(created.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestGetUsersAndIsSetup(t *testing.T) {
|
|
s := newTestBbolt(t)
|
|
defer closeIfBbolt(s)
|
|
users := s.GetUsers()
|
|
if len(users) == 0 {
|
|
t.Error("no users")
|
|
}
|
|
if !s.IsSetup() {
|
|
t.Error("not setup")
|
|
}
|
|
}
|
|
|
|
func TestFidelityMetaSlices(t *testing.T) {
|
|
s := newTestBbolt(t)
|
|
defer closeIfBbolt(s)
|
|
|
|
ph := ProxyHost{DomainNames: nil, Meta: nil}
|
|
c, _ := s.CreateProxyHost(ph)
|
|
// impls normalize to non-nil
|
|
if c.DomainNames == nil || c.Meta == nil {
|
|
t.Error("should have non-nil slices/maps after create")
|
|
}
|
|
g, _ := s.GetProxyHost(c.ID)
|
|
if g.DomainNames == nil {
|
|
t.Error("get returned nil domains")
|
|
}
|
|
|
|
// audit fidelity (CreatedOn + Meta)
|
|
alog := AuditLog{UserID: 1, ObjectType: "t", ObjectID: 1, Action: "a", Meta: map[string]any{"m": 2}}
|
|
added, _ := s.AddAuditLog(alog)
|
|
if added.CreatedOn == "" || added.Meta["m"] != 2 {
|
|
t.Error("audit CreatedOn/Meta fidelity")
|
|
}
|
|
gots := s.GetAuditLogs()
|
|
if len(gots) == 0 || gots[len(gots)-1].Meta == nil {
|
|
t.Error("get audit meta nil")
|
|
}
|
|
|
|
// access list nested slices fidelity
|
|
al := AccessList{Name: "al", Clients: []AccessClient{{Address: "1.2.3.4", Directive: "allow"}}, Items: []AccessItem{{Username: "u", Password: "p"}}}
|
|
ca, _ := s.CreateAccessList(al)
|
|
if len(ca.Clients) == 0 || len(ca.Items) == 0 {
|
|
t.Error("access nested create")
|
|
}
|
|
ga, _ := s.GetAccessList(ca.ID)
|
|
if len(ga.Items) == 0 || len(ga.Clients) == 0 {
|
|
t.Error("access nested get")
|
|
}
|
|
}
|
|
|
|
func TestConcurrency(t *testing.T) {
|
|
s := newTestBbolt(t)
|
|
defer closeIfBbolt(s)
|
|
|
|
var wg sync.WaitGroup
|
|
n := 20
|
|
var mu sync.Mutex
|
|
ids := []int{}
|
|
errs := []error{}
|
|
for i := 0; i < n; i++ {
|
|
wg.Add(1)
|
|
go func(i int) {
|
|
defer wg.Done()
|
|
h, err := s.CreateProxyHost(ProxyHost{DomainNames: []string{"c" + strconv.Itoa(i) + ".com"}})
|
|
_ = s.SetSetting("k", i)
|
|
_, _ = s.AddAuditLog(AuditLog{Action: "cc"})
|
|
mu.Lock()
|
|
ids = append(ids, h.ID)
|
|
if err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
mu.Unlock()
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
if len(errs) > 0 {
|
|
t.Errorf("conc errs: %v", errs)
|
|
}
|
|
if len(ids) != n {
|
|
t.Errorf("expected %d creates, got %d", n, len(ids))
|
|
}
|
|
seen := map[int]bool{}
|
|
for _, id := range ids {
|
|
if seen[id] {
|
|
t.Errorf("dup ID %d in conc creates", id)
|
|
}
|
|
seen[id] = true
|
|
}
|
|
// final fidelity post-conc
|
|
final := s.GetProxyHosts()
|
|
if len(final) != n {
|
|
t.Errorf("post-conc hosts count %d != %d", len(final), n)
|
|
}
|
|
}
|