Refactor storage layer to use key-value template pattern
All checks were successful
continuous-integration/drone/tag Build is passing

Extract common key-value storage operations into internal/storage/kvtpl/
template directory. This consolidates duplicated logic from both leveldb
and inmemory implementations into reusable generic functions.

Key changes:
- Create kvtpl/ package with generic KV storage operations
- Consolidate iterator implementation using generics
- Update migration functions to use new template structure
- Refactor tests to work with new storage interface

This prepares the codebase for easier addition of new storage backends
by providing a consistent template layer.
This commit is contained in:
nemunaire 2026-01-09 17:16:46 +07:00
commit 6add2f220e
45 changed files with 1283 additions and 1834 deletions

View file

@ -1,129 +0,0 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2025 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package inmemory
import (
"git.happydns.org/happyDomain/model"
)
func (s *InMemoryStorage) ListAllAuthUsers() (happydns.Iterator[happydns.UserAuth], error) {
s.mu.Lock()
defer s.mu.Unlock()
return NewInMemoryIterator[happydns.UserAuth](&s.authUsers), nil
}
// ListAuthUsers retrieves the list of known Users.
func (s *InMemoryStorage) ListAuthUsers() (happydns.UserAuths, error) {
s.mu.Lock()
defer s.mu.Unlock()
var users happydns.UserAuths
for _, user := range s.authUsers {
users = append(users, user)
}
return users, nil
}
// GetAuthUser retrieves the User with the given identifier.
func (s *InMemoryStorage) GetAuthUser(id happydns.Identifier) (*happydns.UserAuth, error) {
s.mu.Lock()
defer s.mu.Unlock()
user, exists := s.authUsers[id.String()]
if !exists {
return nil, happydns.ErrAuthUserNotFound
}
return user, nil
}
// GetAuthUserByEmail retrieves the User with the given email address.
func (s *InMemoryStorage) GetAuthUserByEmail(email string) (*happydns.UserAuth, error) {
s.mu.Lock()
defer s.mu.Unlock()
userid, exists := s.authUsersByEmail[email]
if !exists {
return nil, happydns.ErrAuthUserNotFound
}
user, exists := s.authUsers[userid.String()]
if !exists {
return nil, happydns.ErrAuthUserNotFound
}
return user, nil
}
// AuthUserExists checks if the given email address is already associated to an User.
func (s *InMemoryStorage) AuthUserExists(email string) (bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
_, exists := s.authUsersByEmail[email]
return exists, nil
}
// CreateAuthUser creates a record in the database for the given User.
func (s *InMemoryStorage) CreateAuthUser(user *happydns.UserAuth) (err error) {
s.mu.Lock()
defer s.mu.Unlock()
user.Id, err = happydns.NewRandomIdentifier()
s.authUsers[user.Id.String()] = user
s.authUsersByEmail[user.Email] = user.Id
return
}
// UpdateAuthUser updates the fields of the given User.
func (s *InMemoryStorage) UpdateAuthUser(user *happydns.UserAuth) error {
s.mu.Lock()
defer s.mu.Unlock()
s.authUsers[user.Id.String()] = user
s.authUsersByEmail[user.Email] = user.Id
return nil
}
// DeleteAuthUser removes the given User from the database.
func (s *InMemoryStorage) DeleteAuthUser(user *happydns.UserAuth) error {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.authUsers, user.Id.String())
delete(s.authUsersByEmail, user.Email)
return nil
}
// ClearAuthUsers deletes all AuthUsers present in the database.
func (s *InMemoryStorage) ClearAuthUsers() error {
s.mu.Lock()
defer s.mu.Unlock()
s.authUsers = make(map[string]*happydns.UserAuth)
s.authUsersByEmail = make(map[string]happydns.Identifier)
return nil
}

View file

@ -23,6 +23,7 @@ package inmemory
import (
"git.happydns.org/happyDomain/internal/storage"
kv "git.happydns.org/happyDomain/internal/storage/kvtpl"
)
func init() {
@ -30,5 +31,10 @@ func init() {
}
func Instantiate() (storage.Storage, error) {
return NewInMemoryStorage()
db, err := NewInMemoryStorage()
if err != nil {
return nil, err
}
return kv.NewKVDatabase(db)
}

View file

@ -22,16 +22,22 @@
package inmemory
import (
"encoding/json"
"fmt"
"log"
"sort"
"strings"
"sync"
"time"
"git.happydns.org/happyDomain/internal/storage"
"git.happydns.org/happyDomain/model"
)
// InMemoryStorage implements the Storage interface using in-memory data structures.
type InMemoryStorage struct {
mu sync.Mutex
data map[string][]byte // Generic key-value store for KVStorage interface
authUsers map[string]*happydns.UserAuth
authUsersByEmail map[string]happydns.Identifier
domains map[string]*happydns.Domain
@ -49,6 +55,7 @@ type InMemoryStorage struct {
// NewInMemoryStorage creates a new instance of InMemoryStorage.
func NewInMemoryStorage() (*InMemoryStorage, error) {
return &InMemoryStorage{
data: make(map[string][]byte),
authUsers: make(map[string]*happydns.UserAuth),
authUsersByEmail: make(map[string]happydns.Identifier),
domains: make(map[string]*happydns.Domain),
@ -85,3 +92,96 @@ func (s *InMemoryStorage) Close() error {
// No connection to close for in-memory storage.
return nil
}
// DecodeData decodes data from the interface (expected to be []byte) into v.
func (s *InMemoryStorage) DecodeData(data interface{}, v interface{}) error {
b, ok := data.([]byte)
if !ok {
return fmt.Errorf("data to decode are not in []byte format (%T)", data)
}
return json.Unmarshal(b, v)
}
// Has checks if a key exists in the storage.
func (s *InMemoryStorage) Has(key string) (bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
_, exists := s.data[key]
return exists, nil
}
// Get retrieves a value by key and decodes it into v.
func (s *InMemoryStorage) Get(key string, v interface{}) error {
s.mu.Lock()
defer s.mu.Unlock()
data, exists := s.data[key]
if !exists {
return happydns.ErrNotFound
}
return json.Unmarshal(data, v)
}
// Put stores a value with the given key.
func (s *InMemoryStorage) Put(key string, v interface{}) error {
data, err := json.Marshal(v)
if err != nil {
return err
}
s.mu.Lock()
defer s.mu.Unlock()
s.data[key] = data
return nil
}
// FindIdentifierKey finds a unique key with the given prefix.
func (s *InMemoryStorage) FindIdentifierKey(prefix string) (key string, id happydns.Identifier, err error) {
s.mu.Lock()
defer s.mu.Unlock()
for {
id, err = happydns.NewRandomIdentifier()
if err != nil {
return
}
key = fmt.Sprintf("%s%s", prefix, id.String())
if _, exists := s.data[key]; !exists {
return
}
}
}
// Delete removes a key from the storage.
func (s *InMemoryStorage) Delete(key string) error {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.data, key)
return nil
}
// Search returns an iterator for all keys with the given prefix.
func (s *InMemoryStorage) Search(prefix string) storage.Iterator {
s.mu.Lock()
defer s.mu.Unlock()
// Collect all matching keys
var keys []string
for k := range s.data {
if strings.HasPrefix(k, prefix) {
keys = append(keys, k)
}
}
// Sort keys for consistent iteration order
sort.Strings(keys)
// Copy data for the iterator to avoid concurrent access issues
data := make(map[string][]byte, len(keys))
for _, k := range keys {
dataCopy := make([]byte, len(s.data[k]))
copy(dataCopy, s.data[k])
data[k] = dataCopy
}
return NewKVIterator(keys, data)
}

View file

@ -1,183 +0,0 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2025 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package inmemory
import (
"slices"
"git.happydns.org/happyDomain/model"
)
func (s *InMemoryStorage) ListAllDomains() (happydns.Iterator[happydns.Domain], error) {
s.mu.Lock()
defer s.mu.Unlock()
return NewInMemoryIterator[happydns.Domain](&s.domains), nil
}
// ListDomains retrieves all Domains associated to the given User.
func (s *InMemoryStorage) ListDomains(u *happydns.User) ([]*happydns.Domain, error) {
s.mu.Lock()
defer s.mu.Unlock()
var domains []*happydns.Domain
for _, domain := range s.domains {
if domain.Owner.Equals(u.Id) {
domains = append(domains, domain)
}
}
return domains, nil
}
// GetDomain retrieves the Domain with the given id and owned by the given User.
func (s *InMemoryStorage) GetDomain(id happydns.Identifier) (*happydns.Domain, error) {
s.mu.Lock()
defer s.mu.Unlock()
domain, exists := s.domains[id.String()]
if !exists {
return nil, happydns.ErrDomainNotFound
}
return domain, nil
}
// GetDomainByDN is like GetDomain but look for the domain name instead of identifier.
func (s *InMemoryStorage) GetDomainByDN(u *happydns.User, dn string) (ret []*happydns.Domain, err error) {
s.mu.Lock()
defer s.mu.Unlock()
for _, domain := range s.domains {
if domain.DomainName == dn {
ret = append(ret, domain)
}
}
if len(ret) == 0 {
return nil, happydns.ErrDomainNotFound
}
return
}
// CreateDomain creates a record in the database for the given Domain.
func (s *InMemoryStorage) CreateDomain(domain *happydns.Domain) (err error) {
s.mu.Lock()
defer s.mu.Unlock()
domain.Id, err = happydns.NewRandomIdentifier()
s.domains[domain.Id.String()] = domain
return
}
// UpdateDomain updates the fields of the given Domain.
func (s *InMemoryStorage) UpdateDomain(domain *happydns.Domain) error {
s.mu.Lock()
defer s.mu.Unlock()
s.domains[domain.Id.String()] = domain
return nil
}
// DeleteDomain removes the given Domain from the database.
func (s *InMemoryStorage) DeleteDomain(domainid happydns.Identifier) error {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.domains, domainid.String())
return nil
}
// ClearDomains deletes all Domains present in the database.
func (s *InMemoryStorage) ClearDomains() error {
s.mu.Lock()
defer s.mu.Unlock()
s.domains = make(map[string]*happydns.Domain)
return nil
}
// DOMAIN LOGS --------------------------------------------------
func (s *InMemoryStorage) ListAllDomainLogs() (happydns.Iterator[happydns.DomainLogWithDomainId], error) {
s.mu.Lock()
defer s.mu.Unlock()
return NewInMemoryIterator[happydns.DomainLogWithDomainId](&s.domainLogs), nil
}
// ListDomainLogs retrieves the logs for the given Domain.
func (s *InMemoryStorage) ListDomainLogs(domain *happydns.Domain) (dlogs []*happydns.DomainLog, err error) {
s.mu.Lock()
defer s.mu.Unlock()
for _, logid := range s.domainLogsByDomains[domain.Id.String()] {
dlogs = append(dlogs, &s.domainLogs[logid.String()].DomainLog)
}
return
}
// CreateDomainLog creates a log entry for the given Domain.
func (s *InMemoryStorage) CreateDomainLog(domain *happydns.Domain, log *happydns.DomainLog) (err error) {
s.mu.Lock()
defer s.mu.Unlock()
log.Id, err = happydns.NewRandomIdentifier()
if err != nil {
return
}
s.domainLogs[log.Id.String()] = &happydns.DomainLogWithDomainId{DomainLog: *log, DomainId: domain.Id}
s.domainLogsByDomains[domain.Id.String()] = append(s.domainLogsByDomains[domain.Id.String()], &log.Id)
return
}
// UpdateDomainLog updates a log entry for the given Domain.
func (s *InMemoryStorage) UpdateDomainLog(domain *happydns.Domain, log *happydns.DomainLog) error {
s.mu.Lock()
defer s.mu.Unlock()
s.domainLogs[log.Id.String()] = &happydns.DomainLogWithDomainId{DomainLog: *log, DomainId: domain.Id}
return nil
}
// DeleteDomainLog deletes a log entry for the given Domain.
func (s *InMemoryStorage) DeleteDomainLog(domain *happydns.Domain, log *happydns.DomainLog) error {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.domainLogs, log.Id.String())
i := slices.IndexFunc(s.domainLogsByDomains[domain.Id.String()], func(e *happydns.Identifier) bool {
return e.Equals(log.Id)
})
if i == -1 {
return happydns.ErrDomainLogNotFound
}
s.domainLogsByDomains[domain.Id.String()] = append(s.domainLogsByDomains[domain.Id.String()][:i], s.domainLogsByDomains[domain.Id.String()][i+1:]...)
return nil
}

View file

@ -21,81 +21,55 @@
package inmemory
import (
"encoding/json"
"iter"
"maps"
)
// InMemoryIterator provides an iterator over a map[string]*T.
type InMemoryIterator[T any] struct {
origin *map[string]*T
next func() (string, *T, bool)
stop func()
key string
item *T
err error
decode func([]byte, interface{}) error
// KVIterator implements the storage.Iterator interface for in-memory KVStorage.
type KVIterator struct {
keys []string
data map[string][]byte
index int
current string
}
// NewInMemoryIterator constructs an iterator over a map[string]*T.
// You can pass any map like map[string]*UserAuth, map[string]*Domain, etc.
func NewInMemoryIterator[T any](m *map[string]*T) *InMemoryIterator[T] {
next, stop := iter.Pull2(maps.All[map[string]*T](*m))
return &InMemoryIterator[T]{
origin: m,
next: next,
stop: stop,
// NewKVIterator creates a new iterator for the given keys and data.
func NewKVIterator(keys []string, data map[string][]byte) *KVIterator {
return &KVIterator{
keys: keys,
data: data,
index: -1,
}
}
// NewInMemoryIteratorCustomDecode creates a new LevelDBIterator instance for the given LevelDB iterator and decode function.
func NewInMemoryIteratorCustomDecode[T any](m *map[string]*T, decodeFunc func([]byte, interface{}) error) *InMemoryIterator[T] {
next, stop := iter.Pull2(maps.All[map[string]*T](*m))
return &InMemoryIterator[T]{
origin: m,
next: next,
stop: stop,
decode: decodeFunc,
// Next moves the iterator to the next item.
func (it *KVIterator) Next() bool {
it.index++
if it.index >= len(it.keys) {
return false
}
it.current = it.keys[it.index]
return true
}
// Next advances the iterator to the next item.
func (it *InMemoryIterator[T]) Next() (valid bool) {
it.key, it.item, valid = it.next()
return
// Key returns the current key.
func (it *KVIterator) Key() string {
if it.index < 0 || it.index >= len(it.keys) {
return ""
}
return it.current
}
// NextWithError advances the iterator to the next item.
func (it *InMemoryIterator[T]) NextWithError() (valid bool) {
it.key, it.item, valid = it.next()
return
// Value returns the current value.
func (it *KVIterator) Value() interface{} {
if it.index < 0 || it.index >= len(it.keys) {
return []byte{}
}
return it.data[it.current]
}
// Item returns the current item pointed to by the iterator.
func (it *InMemoryIterator[T]) Item() *T {
return it.item
// Valid returns whether the iterator is at a valid position.
func (it *KVIterator) Valid() bool {
return it.index >= 0 && it.index < len(it.keys)
}
// DropItem deletes the key currently pointed to by the iterator.
func (it *InMemoryIterator[T]) DropItem() error {
delete(*it.origin, it.key)
return nil
}
// Raw returns the raw (non-decoded) value at the current iterator position.
// Should only be called after a successful call to Next().
func (it *InMemoryIterator[T]) Raw() []byte {
j, _ := json.Marshal(it.item)
return j
}
// Err returns any error encountered during iteration (always nil here).
func (it *InMemoryIterator[T]) Err() error {
return it.err
}
// Close is a no-op for in-memory iterators, present to satisfy common interfaces.
func (it *InMemoryIterator[T]) Close() {
return
// Release releases the iterator resources.
func (it *KVIterator) Release() {
// No resources to release for in-memory iterator
}

View file

@ -1,118 +0,0 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2025 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package inmemory
import (
"encoding/json"
"git.happydns.org/happyDomain/model"
)
func (s *InMemoryStorage) ListAllProviders() (happydns.Iterator[happydns.ProviderMessage], error) {
s.mu.Lock()
defer s.mu.Unlock()
return NewInMemoryIterator[happydns.ProviderMessage](&s.providers), nil
}
// ListProviders retrieves all providers owned by the given User.
func (s *InMemoryStorage) ListProviders(u *happydns.User) (happydns.ProviderMessages, error) {
s.mu.Lock()
defer s.mu.Unlock()
var providers happydns.ProviderMessages
for _, provider := range s.providers {
if provider.Owner.Equals(u.Id) {
providers = append(providers, provider)
}
}
return providers, nil
}
// GetProvider retrieves the full Provider with the given identifier and owner.
func (s *InMemoryStorage) GetProvider(id happydns.Identifier) (*happydns.ProviderMessage, error) {
s.mu.Lock()
defer s.mu.Unlock()
provider, exists := s.providers[id.String()]
if !exists {
return nil, happydns.ErrProviderNotFound
}
return provider, nil
}
// CreateProvider creates a record in the database for the given Provider.
func (s *InMemoryStorage) CreateProvider(p *happydns.Provider) error {
s.mu.Lock()
defer s.mu.Unlock()
message, err := json.Marshal(p.Provider)
if err != nil {
return err
}
p.ProviderMeta.Id, err = happydns.NewRandomIdentifier()
s.providers[p.ProviderMeta.Id.String()] = &happydns.ProviderMessage{
ProviderMeta: p.ProviderMeta,
Provider: message,
}
return nil
}
// UpdateProvider updates the fields of the given Provider.
func (s *InMemoryStorage) UpdateProvider(p *happydns.Provider) error {
s.mu.Lock()
defer s.mu.Unlock()
message, err := json.Marshal(p.Provider)
if err != nil {
return err
}
s.providers[p.ProviderMeta.Id.String()] = &happydns.ProviderMessage{
ProviderMeta: p.ProviderMeta,
Provider: message,
}
return nil
}
// DeleteProvider removes the given Provider from the database.
func (s *InMemoryStorage) DeleteProvider(id happydns.Identifier) error {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.providers, id.String())
return nil
}
// ClearProviders deletes all Providers present in the database.
func (s *InMemoryStorage) ClearProviders() error {
s.mu.Lock()
defer s.mu.Unlock()
s.providers = make(map[string]*happydns.ProviderMessage)
return nil
}

View file

@ -1,101 +0,0 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2025 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package inmemory
import (
"git.happydns.org/happyDomain/model"
)
func (s *InMemoryStorage) ListAllSessions() (happydns.Iterator[happydns.Session], error) {
s.mu.Lock()
defer s.mu.Unlock()
return NewInMemoryIterator[happydns.Session](&s.sessions), nil
}
// GetSession retrieves the Session with the given identifier.
func (s *InMemoryStorage) GetSession(id string) (*happydns.Session, error) {
s.mu.Lock()
defer s.mu.Unlock()
session, exists := s.sessions[id]
if !exists {
return nil, happydns.ErrSessionNotFound
}
return session, nil
}
// ListAuthUserSessions retrieves all Session for the given AuthUser.
func (s *InMemoryStorage) ListAuthUserSessions(user *happydns.UserAuth) (sessions []*happydns.Session, ess error) {
s.mu.Lock()
defer s.mu.Unlock()
for _, session := range s.sessions {
if user.Id.Equals(session.IdUser) {
sessions = append(sessions, session)
}
}
return sessions, nil
}
// ListUserSessions retrieves all Session for the given User.
func (s *InMemoryStorage) ListUserSessions(userid happydns.Identifier) (sessions []*happydns.Session, ess error) {
s.mu.Lock()
defer s.mu.Unlock()
for _, session := range s.sessions {
if userid.Equals(session.IdUser) {
sessions = append(sessions, session)
}
}
return sessions, nil
}
// UpdateSession updates the fields of the given Session.
func (s *InMemoryStorage) UpdateSession(session *happydns.Session) error {
s.mu.Lock()
defer s.mu.Unlock()
s.sessions[session.Id] = session
return nil
}
// DeleteSession removes the given Session from the database.
func (s *InMemoryStorage) DeleteSession(id string) error {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.sessions, id)
return nil
}
// ClearSessions deletes all Sessions present in the database.
func (s *InMemoryStorage) ClearSessions() error {
s.mu.Lock()
defer s.mu.Unlock()
s.sessions = make(map[string]*happydns.Session)
return nil
}

View file

@ -1,105 +0,0 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2025 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package inmemory
import (
"git.happydns.org/happyDomain/model"
)
func (s *InMemoryStorage) ListAllUsers() (happydns.Iterator[happydns.User], error) {
s.mu.Lock()
defer s.mu.Unlock()
return NewInMemoryIterator[happydns.User](&s.users), nil
}
// ListUsers retrieves the list of known Users.
func (s *InMemoryStorage) ListUsers() (users []*happydns.User, err error) {
s.mu.Lock()
defer s.mu.Unlock()
for _, user := range s.users {
users = append(users, user)
}
return users, nil
}
// GetUser retrieves the User with the given identifier.
func (s *InMemoryStorage) GetUser(id happydns.Identifier) (*happydns.User, error) {
s.mu.Lock()
defer s.mu.Unlock()
user, exists := s.users[id.String()]
if !exists {
return nil, happydns.ErrUserNotFound
}
return user, nil
}
// GetUserByEmail retrieves the User with the given email address.
func (s *InMemoryStorage) GetUserByEmail(email string) (*happydns.User, error) {
s.mu.Lock()
defer s.mu.Unlock()
user, exists := s.usersByEmail[email]
if !exists {
return nil, happydns.ErrUserNotFound
}
return user, nil
}
// CreateOrUpdateUser updates the fields of the given User.
func (s *InMemoryStorage) CreateOrUpdateUser(user *happydns.User) error {
s.mu.Lock()
defer s.mu.Unlock()
s.users[user.Id.String()] = user
s.usersByEmail[user.Email] = user
return nil
}
// DeleteUser removes the given User from the database.
func (s *InMemoryStorage) DeleteUser(userid happydns.Identifier) error {
s.mu.Lock()
defer s.mu.Unlock()
if user, exists := s.users[userid.String()]; exists {
delete(s.users, userid.String())
delete(s.usersByEmail, user.Email)
}
return nil
}
// ClearUsers deletes all Users present in the database.
func (s *InMemoryStorage) ClearUsers() error {
s.mu.Lock()
defer s.mu.Unlock()
s.users = make(map[string]*happydns.User)
s.usersByEmail = make(map[string]*happydns.User)
return nil
}

View file

@ -1,116 +0,0 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2025 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package inmemory
import (
"encoding/json"
"git.happydns.org/happyDomain/model"
)
func (s *InMemoryStorage) ListAllZones() (happydns.Iterator[happydns.ZoneMessage], error) {
s.mu.Lock()
defer s.mu.Unlock()
return NewInMemoryIterator[happydns.ZoneMessage](&s.zones), nil
}
// GetZoneMeta retrieves metadata of the Zone with the given identifier.
func (s *InMemoryStorage) GetZoneMeta(id happydns.Identifier) (*happydns.ZoneMeta, error) {
s.mu.Lock()
defer s.mu.Unlock()
zone, exists := s.zones[id.String()]
if !exists {
return nil, happydns.ErrZoneNotFound
}
return &zone.ZoneMeta, nil
}
// GetZone retrieves the full Zone (including Services and metadata) which have the given identifier.
func (s *InMemoryStorage) GetZone(id happydns.Identifier) (*happydns.ZoneMessage, error) {
s.mu.Lock()
defer s.mu.Unlock()
zone, exists := s.zones[id.String()]
if !exists {
return nil, happydns.ErrZoneNotFound
}
return zone, nil
}
// CreateZone creates a record in the database for the given Zone.
func (s *InMemoryStorage) CreateZone(zone *happydns.Zone) (err error) {
zone.ZoneMeta.Id, err = happydns.NewRandomIdentifier()
if err != nil {
return
}
return s.UpdateZone(zone)
}
// UpdateZone updates the fields of the given Zone.
func (s *InMemoryStorage) UpdateZone(zone *happydns.Zone) error {
s.mu.Lock()
defer s.mu.Unlock()
zmsg := &happydns.ZoneMessage{
ZoneMeta: zone.ZoneMeta,
Services: map[happydns.Subdomain][]*happydns.ServiceMessage{},
}
for subdn, services := range zone.Services {
for _, service := range services {
message, err := json.Marshal(service.Service)
if err != nil {
return err
}
zmsg.Services[subdn] = append(zmsg.Services[subdn], &happydns.ServiceMessage{
ServiceMeta: service.ServiceMeta,
Service: message,
})
}
}
s.zones[zone.Id.String()] = zmsg
return nil
}
// DeleteZone removes the given Zone from the database.
func (s *InMemoryStorage) DeleteZone(id happydns.Identifier) error {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.zones, id.String())
return nil
}
// ClearZones deletes all Zones present in the database.
func (s *InMemoryStorage) ClearZones() error {
s.mu.Lock()
defer s.mu.Unlock()
s.zones = make(map[string]*happydns.ZoneMessage)
return nil
}

View file

@ -30,6 +30,7 @@ import (
"git.happydns.org/happyDomain/internal/usecase/session"
"git.happydns.org/happyDomain/internal/usecase/user"
"git.happydns.org/happyDomain/internal/usecase/zone"
"git.happydns.org/happyDomain/model"
)
type ProviderAndDomainStorage interface {
@ -56,3 +57,22 @@ type Storage interface {
// Close shutdown the connection with the database and releases all structure.
Close() error
}
type Iterator interface {
Release()
Next() bool
Valid() bool
Key() string
Value() interface{}
}
type KVStorage interface {
Close() error
DecodeData(i interface{}, v interface{}) error
Has(key string) (bool, error)
Get(key string, v interface{}) error
Put(key string, v interface{}) error
FindIdentifierKey(prefix string) (key string, id happydns.Identifier, err error)
Delete(key string) error
Search(prefix string) Iterator
}

View file

@ -1,5 +1,5 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2024 happyDomain
// Copyright (c) 2020-2025 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
@ -25,35 +25,33 @@ import (
"errors"
"fmt"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/util"
"git.happydns.org/happyDomain/model"
)
func (s *LevelDBStorage) ListAllAuthUsers() (happydns.Iterator[happydns.UserAuth], error) {
iter := s.search("auth-")
return NewLevelDBIterator[happydns.UserAuth](s.db, iter), nil
func (s *KVStorage) ListAllAuthUsers() (happydns.Iterator[happydns.UserAuth], error) {
iter := s.db.Search("auth-")
return NewKVIterator[happydns.UserAuth](s.db, iter), nil
}
func (s *LevelDBStorage) getAuthUser(key string) (*happydns.UserAuth, error) {
func (s *KVStorage) getAuthUser(key string) (*happydns.UserAuth, error) {
u := &happydns.UserAuth{}
err := s.get(key, &u)
if errors.Is(err, leveldb.ErrNotFound) {
err := s.db.Get(key, &u)
if errors.Is(err, happydns.ErrNotFound) {
return nil, happydns.ErrAuthUserNotFound
}
return u, err
}
func (s *LevelDBStorage) GetAuthUser(id happydns.Identifier) (u *happydns.UserAuth, err error) {
func (s *KVStorage) GetAuthUser(id happydns.Identifier) (u *happydns.UserAuth, err error) {
return s.getAuthUser(fmt.Sprintf("auth-%s", id.String()))
}
func (s *LevelDBStorage) GetAuthUserByEmail(email string) (*happydns.UserAuth, error) {
func (s *KVStorage) GetAuthUserByEmail(email string) (*happydns.UserAuth, error) {
users, err := s.ListAllAuthUsers()
if err != nil {
return nil, err
}
defer users.Close()
for users.Next() {
user := users.Item()
@ -65,11 +63,12 @@ func (s *LevelDBStorage) GetAuthUserByEmail(email string) (*happydns.UserAuth, e
return nil, fmt.Errorf("Unable to find user with email address '%s'.", email)
}
func (s *LevelDBStorage) AuthUserExists(email string) (bool, error) {
func (s *KVStorage) AuthUserExists(email string) (bool, error) {
users, err := s.ListAllAuthUsers()
if err != nil {
return false, err
}
defer users.Close()
for users.Next() {
user := users.Item()
@ -81,46 +80,37 @@ func (s *LevelDBStorage) AuthUserExists(email string) (bool, error) {
return false, nil
}
func (s *LevelDBStorage) CreateAuthUser(u *happydns.UserAuth) error {
key, id, err := s.findIdentifierKey("auth-")
func (s *KVStorage) CreateAuthUser(u *happydns.UserAuth) error {
key, id, err := s.db.FindIdentifierKey("auth-")
if err != nil {
return err
}
u.Id = id
return s.put(key, u)
return s.db.Put(key, u)
}
func (s *LevelDBStorage) UpdateAuthUser(u *happydns.UserAuth) error {
return s.put(fmt.Sprintf("auth-%s", u.Id.String()), u)
func (s *KVStorage) UpdateAuthUser(u *happydns.UserAuth) error {
return s.db.Put(fmt.Sprintf("auth-%s", u.Id.String()), u)
}
func (s *LevelDBStorage) DeleteAuthUser(u *happydns.UserAuth) error {
return s.delete(fmt.Sprintf("auth-%s", u.Id.String()))
func (s *KVStorage) DeleteAuthUser(u *happydns.UserAuth) error {
return s.db.Delete(fmt.Sprintf("auth-%s", u.Id.String()))
}
func (s *LevelDBStorage) ClearAuthUsers() error {
tx, err := s.db.OpenTransaction()
func (s *KVStorage) ClearAuthUsers() error {
iter, err := s.ListAllAuthUsers()
if err != nil {
return err
}
iter := tx.NewIterator(util.BytesPrefix([]byte("auth-")), nil)
defer iter.Release()
defer iter.Close()
for iter.Next() {
err = tx.Delete(iter.Key(), nil)
err = s.db.Delete(iter.Key())
if err != nil {
tx.Discard()
return err
}
}
err = tx.Commit()
if err != nil {
tx.Discard()
return err
}
return nil
}

View file

@ -0,0 +1,122 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2025 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package database
import (
"errors"
"fmt"
"strings"
"git.happydns.org/happyDomain/internal/storage"
"git.happydns.org/happyDomain/model"
)
// DomainLogIterator wraps KVIterator to populate DomainId from the key
type DomainLogIterator struct {
*KVIterator[happydns.DomainLogWithDomainId]
}
// NewDomainLogIterator creates a new DomainLogIterator
func NewDomainLogIterator(db storage.KVStorage, iter storage.Iterator) *DomainLogIterator {
return &DomainLogIterator{
KVIterator: NewKVIterator[happydns.DomainLogWithDomainId](db, iter),
}
}
// Next advances the iterator and extracts the DomainId from the key
func (it *DomainLogIterator) Next() bool {
if it.KVIterator.Next() {
// Extract domain ID from key
key := it.Key()
st := strings.Split(key, "|")
if len(st) >= 3 && it.item != nil {
domainId, err := happydns.NewIdentifierFromString(st[1])
if err == nil {
it.item.DomainId = domainId
}
}
return true
}
return false
}
func (s *KVStorage) ListAllDomainLogs() (happydns.Iterator[happydns.DomainLogWithDomainId], error) {
iter := s.db.Search("domain.log|")
return NewDomainLogIterator(s.db, iter), nil
}
func (s *KVStorage) ListDomainLogs(domain *happydns.Domain) (logs []*happydns.DomainLog, err error) {
iter := s.db.Search(fmt.Sprintf("domain.log|%s|", domain.Id.String()))
defer iter.Release()
for iter.Next() {
var z happydns.DomainLog
err = s.db.DecodeData(iter.Value(), &z)
if err != nil {
return
}
logs = append(logs, &z)
}
return
}
func (s *KVStorage) getDomainLog(id string) (l *happydns.DomainLog, d *happydns.Domain, err error) {
l = &happydns.DomainLog{}
err = s.db.Get(id, l)
if errors.Is(err, happydns.ErrNotFound) {
return nil, nil, happydns.ErrDomainLogNotFound
}
st := strings.Split(id, "|")
if len(st) < 3 {
return
}
d = &happydns.Domain{}
err = s.db.Get(fmt.Sprintf("domain-%s", st[1]), d)
if errors.Is(err, happydns.ErrNotFound) {
return nil, nil, happydns.ErrDomainNotFound
}
return
}
func (s *KVStorage) CreateDomainLog(d *happydns.Domain, l *happydns.DomainLog) error {
key, id, err := s.db.FindIdentifierKey(fmt.Sprintf("domain.log|%s|", d.Id.String()))
if err != nil {
return err
}
l.Id = id
return s.db.Put(key, l)
}
func (s *KVStorage) UpdateDomainLog(d *happydns.Domain, l *happydns.DomainLog) error {
return s.db.Put(fmt.Sprintf("domain.log|%s|%s", d.Id.String(), l.Id.String()), l)
}
func (s *KVStorage) DeleteDomainLog(d *happydns.Domain, l *happydns.DomainLog) error {
return s.db.Delete(fmt.Sprintf("domain.log|%s|%s", d.Id.String(), l.Id.String()))
}

View file

@ -1,5 +1,5 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2024 happyDomain
// Copyright (c) 2020-2025 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
@ -26,25 +26,22 @@ import (
"errors"
"fmt"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/util"
"git.happydns.org/happyDomain/model"
)
func (s *LevelDBStorage) ListAllDomains() (happydns.Iterator[happydns.Domain], error) {
iter := s.search("domain-")
return NewLevelDBIterator[happydns.Domain](s.db, iter), nil
func (s *KVStorage) ListAllDomains() (happydns.Iterator[happydns.Domain], error) {
iter := s.db.Search("domain-")
return NewKVIterator[happydns.Domain](s.db, iter), nil
}
func (s *LevelDBStorage) ListDomains(u *happydns.User) (domains []*happydns.Domain, err error) {
iter := s.search("domain-")
func (s *KVStorage) ListDomains(u *happydns.User) (domains []*happydns.Domain, err error) {
iter := s.db.Search("domain-")
defer iter.Release()
for iter.Next() {
var z happydns.Domain
err = decodeData(iter.Value(), &z)
err = s.db.DecodeData(iter.Value(), &z)
if err != nil {
return
}
@ -57,20 +54,20 @@ func (s *LevelDBStorage) ListDomains(u *happydns.User) (domains []*happydns.Doma
return
}
func (s *LevelDBStorage) getDomain(id string) (*happydns.Domain, error) {
func (s *KVStorage) getDomain(id string) (*happydns.Domain, error) {
domain := &happydns.Domain{}
err := s.get(id, domain)
if errors.Is(err, leveldb.ErrNotFound) {
err := s.db.Get(id, domain)
if errors.Is(err, happydns.ErrNotFound) {
return nil, happydns.ErrDomainNotFound
}
return domain, err
}
func (s *LevelDBStorage) GetDomain(id happydns.Identifier) (*happydns.Domain, error) {
func (s *KVStorage) GetDomain(id happydns.Identifier) (*happydns.Domain, error) {
return s.getDomain(fmt.Sprintf("domain-%s", id.String()))
}
func (s *LevelDBStorage) GetDomainByDN(u *happydns.User, dn string) ([]*happydns.Domain, error) {
func (s *KVStorage) GetDomainByDN(u *happydns.User, dn string) ([]*happydns.Domain, error) {
domains, err := s.ListDomains(u)
if err != nil {
return nil, err
@ -84,57 +81,48 @@ func (s *LevelDBStorage) GetDomainByDN(u *happydns.User, dn string) ([]*happydns
}
if len(ret) == 0 {
return nil, leveldb.ErrNotFound
return nil, happydns.ErrNotFound
}
return ret, nil
}
func (s *LevelDBStorage) CreateDomain(z *happydns.Domain) error {
key, id, err := s.findIdentifierKey("domain-")
func (s *KVStorage) CreateDomain(z *happydns.Domain) error {
key, id, err := s.db.FindIdentifierKey("domain-")
if err != nil {
return err
}
z.Id = id
return s.put(key, z)
return s.db.Put(key, z)
}
func (s *LevelDBStorage) UpdateDomain(z *happydns.Domain) error {
return s.put(fmt.Sprintf("domain-%s", z.Id.String()), z)
func (s *KVStorage) UpdateDomain(z *happydns.Domain) error {
return s.db.Put(fmt.Sprintf("domain-%s", z.Id.String()), z)
}
func (s *LevelDBStorage) DeleteDomain(zId happydns.Identifier) error {
return s.delete(fmt.Sprintf("domain-%s", zId.String()))
func (s *KVStorage) DeleteDomain(zId happydns.Identifier) error {
return s.db.Delete(fmt.Sprintf("domain-%s", zId.String()))
}
func (s *LevelDBStorage) ClearDomains() error {
func (s *KVStorage) ClearDomains() error {
err := s.ClearZones()
if err != nil {
return err
}
tx, err := s.db.OpenTransaction()
iter, err := s.ListAllDomains()
if err != nil {
return err
}
iter := tx.NewIterator(util.BytesPrefix([]byte("domain-")), nil)
defer iter.Release()
defer iter.Close()
for iter.Next() {
err = tx.Delete(iter.Key(), nil)
err = s.db.Delete(iter.Key())
if err != nil {
tx.Discard()
return err
}
}
err = tx.Commit()
if err != nil {
tx.Discard()
return err
}
return nil
}

View file

@ -22,27 +22,26 @@
package database
import (
"errors"
"time"
"github.com/syndtr/goleveldb/leveldb"
"git.happydns.org/happyDomain/model"
)
func (s *LevelDBStorage) InsightsRun() error {
return s.put("insights", time.Now())
func (s *KVStorage) InsightsRun() error {
return s.db.Put("insights", time.Now())
}
func (s *LevelDBStorage) LastInsightsRun() (t *time.Time, instance happydns.Identifier, err error) {
err = s.get("insights.instance-id", &instance)
if err == leveldb.ErrNotFound {
func (s *KVStorage) LastInsightsRun() (t *time.Time, instance happydns.Identifier, err error) {
err = s.db.Get("insights.instance-id", &instance)
if errors.Is(err, happydns.ErrNotFound) {
// No instance ID defined, set one
instance, err = happydns.NewRandomIdentifier()
if err != nil {
return
}
err = s.put("insights.instance-id", instance)
err = s.db.Put("insights.instance-id", instance)
if err != nil {
return
}
@ -51,8 +50,8 @@ func (s *LevelDBStorage) LastInsightsRun() (t *time.Time, instance happydns.Iden
}
t = new(time.Time)
err = s.get("insights", &t)
if err == leveldb.ErrNotFound {
err = s.db.Get("insights", &t)
if errors.Is(err, happydns.ErrNotFound) {
t = nil
err = nil
} else if err != nil {

View file

@ -0,0 +1,120 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2025 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package database
import (
"fmt"
"log"
"git.happydns.org/happyDomain/internal/storage"
)
// KVIterator is a generic implementation of Iterator for LevelDB.
type KVIterator[T any] struct {
db storage.KVStorage
iter storage.Iterator
err error
item *T
}
// NewKVIterator creates a new KVIterator instance for the given iterator.
func NewKVIterator[T any](db storage.KVStorage, iter storage.Iterator) *KVIterator[T] {
return &KVIterator[T]{
db: db,
iter: iter,
}
}
// Next moves the iterator to the next valid item.
// Skips items that fail to decode and logs the error.
func (it *KVIterator[T]) Next() bool {
for it.iter.Next() {
var value T
err := it.db.DecodeData(it.iter.Value(), &value)
if err != nil {
log.Printf("KVIterator: error decoding item at key %q: %s", it.iter.Key(), err)
it.err = err
continue
}
it.item = &value
return true
}
return false
}
// NextWithError advances the iterator to the next item, on decode error it doesn't continue to the next item.
// Returns true if there is a next item, false otherwise.
func (it *KVIterator[T]) NextWithError() bool {
if it.iter.Next() {
var value T
err := it.db.DecodeData(it.iter.Value(), &value)
if err != nil {
it.err = err
it.item = nil
} else {
it.err = nil
it.item = &value
}
return true
}
return false
}
// Item returns the current item from the iterator.
// Only valid after a successful call to Next().
func (it *KVIterator[T]) Item() *T {
return it.item
}
// DropItem deletes the key currently pointed to by the iterator.
func (it *KVIterator[T]) DropItem() error {
if it.iter == nil || !it.iter.Valid() {
return fmt.Errorf("DropItem: iterator is not valid")
}
return it.db.Delete(it.iter.Key())
}
// Raw returns the raw (non-decoded) value at the current iterator position.
// Should only be called after a successful call to Next().
func (it *KVIterator[T]) Raw() interface{} {
if it.iter == nil || !it.iter.Valid() {
return []byte{}
}
return it.iter.Value()
}
func (it *KVIterator[T]) Key() string {
if it.iter == nil || !it.iter.Valid() {
return ""
}
return it.iter.Key()
}
// Err returns the first error encountered during iteration, if any.
func (it *KVIterator[T]) Err() error {
return it.err
}
// Close releases resources held by the underlying LevelDB iterator.
func (it *KVIterator[T]) Close() {
it.iter.Release()
}

View file

@ -0,0 +1,112 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2025 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package database
import (
"bytes"
"errors"
"fmt"
"git.happydns.org/happyDomain/model"
)
func (s *KVStorage) ListAllProviders() (happydns.Iterator[happydns.ProviderMessage], error) {
iter := s.db.Search("provider-")
return NewKVIterator[happydns.ProviderMessage](s.db, iter), nil
}
func (s *KVStorage) getProviderMeta(id happydns.Identifier) (*happydns.ProviderMessage, error) {
srcMsg := &happydns.ProviderMessage{}
err := s.db.Get(id.String(), srcMsg)
if errors.Is(err, happydns.ErrNotFound) {
return nil, happydns.ErrProviderNotFound
}
return srcMsg, err
}
func (s *KVStorage) ListProviders(u *happydns.User) (srcs happydns.ProviderMessages, err error) {
iter, err := s.ListAllProviders()
if err != nil {
return nil, err
}
defer iter.Close()
for iter.Next() {
srcMsg := iter.Item()
if !bytes.Equal(srcMsg.Owner, u.Id) {
continue
}
srcs = append(srcs, srcMsg)
}
return
}
func (s *KVStorage) GetProvider(id happydns.Identifier) (*happydns.ProviderMessage, error) {
var prvdMsg happydns.ProviderMessage
err := s.db.Get(fmt.Sprintf("provider-%s", id.String()), &prvdMsg)
if errors.Is(err, happydns.ErrNotFound) {
return nil, happydns.ErrProviderNotFound
}
if err != nil {
return nil, err
}
return &prvdMsg, nil
}
func (s *KVStorage) CreateProvider(prvd *happydns.Provider) error {
key, id, err := s.db.FindIdentifierKey("provider-")
if err != nil {
return err
}
prvd.Id = id
return s.db.Put(key, prvd)
}
func (s *KVStorage) UpdateProvider(prvd *happydns.Provider) error {
return s.db.Put(fmt.Sprintf("provider-%s", prvd.Id.String()), prvd)
}
func (s *KVStorage) DeleteProvider(prvdId happydns.Identifier) error {
return s.db.Delete(fmt.Sprintf("provider-%s", prvdId.String()))
}
func (s *KVStorage) ClearProviders() error {
iter, err := s.ListAllProviders()
if err != nil {
return err
}
defer iter.Close()
for iter.Next() {
err = s.db.Delete(iter.Key())
if err != nil {
return err
}
}
return nil
}

View file

@ -0,0 +1,107 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2025 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package database
import (
"errors"
"fmt"
"git.happydns.org/happyDomain/model"
)
func (s *KVStorage) ListAllSessions() (happydns.Iterator[happydns.Session], error) {
iter := s.db.Search("user.session-")
return NewKVIterator[happydns.Session](s.db, iter), nil
}
func (s *KVStorage) getSession(id string) (*happydns.Session, error) {
session := &happydns.Session{}
err := s.db.Get(id, &session)
if errors.Is(err, happydns.ErrNotFound) {
return nil, happydns.ErrSessionNotFound
}
return session, err
}
func (s *KVStorage) GetSession(id string) (session *happydns.Session, err error) {
return s.getSession(fmt.Sprintf("user.session-%s", id))
}
func (s *KVStorage) ListAuthUserSessions(user *happydns.UserAuth) (sessions []*happydns.Session, err error) {
iter := s.db.Search("user.session-")
defer iter.Release()
for iter.Next() {
var session happydns.Session
err = s.db.DecodeData(iter.Value(), &session)
if err != nil {
return
}
if session.IdUser.Equals(user.Id) {
sessions = append(sessions, &session)
}
}
return
}
func (s *KVStorage) ListUserSessions(userid happydns.Identifier) (sessions []*happydns.Session, err error) {
iter := s.db.Search("user.session-")
defer iter.Release()
for iter.Next() {
var session happydns.Session
err = s.db.DecodeData(iter.Value(), &session)
if err != nil {
return
}
if session.IdUser.Equals(userid) {
sessions = append(sessions, &session)
}
}
return
}
func (s *KVStorage) UpdateSession(session *happydns.Session) error {
return s.db.Put(fmt.Sprintf("user.session-%s", session.Id), session)
}
func (s *KVStorage) DeleteSession(id string) error {
return s.db.Delete(fmt.Sprintf("user.session-%s", id))
}
func (s *KVStorage) ClearSessions() error {
iter := s.db.Search("user.session-")
defer iter.Release()
for iter.Next() {
err := s.db.Delete(iter.Key())
if err != nil {
return err
}
}
return nil
}

View file

@ -19,37 +19,22 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package inmemory
package database
import (
"time"
"git.happydns.org/happyDomain/model"
"git.happydns.org/happyDomain/internal/storage"
)
// InsightsRun registers a insights process run just now.
func (s *InMemoryStorage) InsightsRun() error {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now()
s.lastInsightsRun = &now
return nil
type KVStorage struct {
db storage.KVStorage
}
// LastInsightsRun gets the last time insights process run.
func (s *InMemoryStorage) LastInsightsRun() (*time.Time, happydns.Identifier, error) {
s.mu.Lock()
defer s.mu.Unlock()
if s.lastInsightsID == nil {
instance, err := happydns.NewRandomIdentifier()
if err != nil {
return nil, nil, err
}
s.lastInsightsID = instance
}
return s.lastInsightsRun, s.lastInsightsID, nil
func NewKVDatabase(impl storage.KVStorage) (storage.Storage, error) {
return &KVStorage{
impl,
}, nil
}
func (s *KVStorage) Close() error {
return s.db.Close()
}

View file

@ -26,7 +26,7 @@ import (
"log"
)
func migrateFrom0(s *LevelDBStorage) (err error) {
func migrateFrom0(s *KVStorage) (err error) {
err = migrateFrom0_sourcesProvider(s)
if err != nil {
return
@ -47,14 +47,20 @@ type sourceMeta struct {
Comment string `json:"_comment,omitempty"`
}
func migrateFrom0_sourcesProvider(s *LevelDBStorage) (err error) {
iter := s.search("source-")
func migrateFrom0_sourcesProvider(s *KVStorage) (err error) {
iter := s.db.Search("source-")
defer iter.Release()
for iter.Next() {
src := iter.Value()
for src[0] == '"' {
err = decodeData(src, &src)
srcRaw := iter.Value()
src, ok := srcRaw.([]byte)
if !ok {
log.Printf("Migrating v0 -> v1: skip %s (not bytes)...", iter.Key())
continue
}
for len(src) > 0 && src[0] == '"' {
err = s.db.DecodeData(src, &src)
if err != nil {
return
}
@ -63,7 +69,7 @@ func migrateFrom0_sourcesProvider(s *LevelDBStorage) (err error) {
src = bytes.Replace(src, []byte("\"Source\":"), []byte("\"Provider\":"), 1)
var srcMeta sourceMeta
err = decodeData(src, &srcMeta)
err = s.db.DecodeData(src, &srcMeta)
if err != nil {
return
}
@ -101,16 +107,16 @@ func migrateFrom0_sourcesProvider(s *LevelDBStorage) (err error) {
}
}
newKey := bytes.Replace(iter.Key(), []byte("source-"), []byte("provider-"), 1)
newKey := string(bytes.Replace([]byte(iter.Key()), []byte("source-"), []byte("provider-"), 1))
log.Printf("Migrating v0 -> v1: %s to %s (%s)...", iter.Key(), newKey, newType)
err = s.db.Put(newKey, src, nil)
err = s.db.Put(newKey, src)
if err != nil {
return
}
err = s.delete(string(iter.Key()))
err = s.db.Delete(iter.Key())
if err != nil {
return
}
@ -119,18 +125,23 @@ func migrateFrom0_sourcesProvider(s *LevelDBStorage) (err error) {
return
}
func migrateFrom0_reparentDomains(s *LevelDBStorage) (err error) {
iter := s.search("domain-")
func migrateFrom0_reparentDomains(s *KVStorage) (err error) {
iter := s.db.Search("domain-")
defer iter.Release()
for iter.Next() {
domstr := iter.Value()
domRaw := iter.Value()
domstr, ok := domRaw.([]byte)
if !ok {
log.Printf("Migrating v0 -> v1: skip %s (not bytes)...", iter.Key())
continue
}
domstr = bytes.Replace(domstr, []byte("\"id_source\":"), []byte("\"id_provider\":"), 1)
log.Printf("Migrating v0 -> v1: %s...", iter.Key())
err = s.db.Put(iter.Key(), domstr, nil)
err = s.db.Put(iter.Key(), domstr)
if err != nil {
return
}

View file

@ -32,7 +32,7 @@ import (
"git.happydns.org/happyDomain/model"
)
func migrateFrom1(s *LevelDBStorage) (err error) {
func migrateFrom1(s *KVStorage) (err error) {
err = migrateFrom1_users_tree(s)
if err != nil {
return
@ -61,19 +61,19 @@ func genUserIdv2(input int64) (string, []byte, error) {
return hex.EncodeToString(decoded), decoded, err
}
func migrateFrom1_users_tree(s *LevelDBStorage) (err error) {
iter := s.search("user-")
func migrateFrom1_users_tree(s *KVStorage) (err error) {
iter := s.db.Search("user-")
defer iter.Release()
for iter.Next() {
var user userV1
err = decodeData(iter.Value(), &user)
err = s.db.DecodeData(iter.Value(), &user)
if err != nil {
return
}
newId, idRaw, errr := genUserIdv2(user.Id)
if err != nil {
if errr != nil {
log.Printf("Migrating v1 -> v2: %s: unable to calculate new ID: %s", iter.Key(), errr.Error())
continue
} else if len(idRaw) == 0 {
@ -106,17 +106,17 @@ func migrateFrom1_users_tree(s *LevelDBStorage) (err error) {
log.Printf("Migrating v1 -> v2: %s to user-%x...", iter.Key(), idRaw)
err = s.put(fmt.Sprintf("user-%x", idRaw), newUser)
err = s.db.Put(fmt.Sprintf("user-%x", idRaw), newUser)
if err != nil {
return
}
err = s.put(fmt.Sprintf("auth-%x", idRaw), user4auth)
err = s.db.Put(fmt.Sprintf("auth-%x", idRaw), user4auth)
if err != nil {
return
}
err = s.delete(string(iter.Key()))
err = s.db.Delete(iter.Key())
if err != nil {
return
}
@ -127,27 +127,35 @@ func migrateFrom1_users_tree(s *LevelDBStorage) (err error) {
migrateFrom1_provider(s, user.Id, newId),
migrateFrom1_zone(s, user.Id, newId),
)
if err != nil {
return
}
}
return
}
func migrateFrom1_domains(s *LevelDBStorage, oldUserId int64, newUserId string) (err error) {
func migrateFrom1_domains(s *KVStorage, oldUserId int64, newUserId string) (err error) {
oldIdStr := []byte(fmt.Sprintf("\"id_owner\":%d", oldUserId))
newIdStr := []byte(fmt.Sprintf("\"id_owner\":\"%s\"", newUserId))
iter := s.search("domain-")
iter := s.db.Search("domain-")
defer iter.Release()
for iter.Next() {
domstr := iter.Value()
domRaw := iter.Value()
domstr, ok := domRaw.([]byte)
if !ok {
log.Printf("Migrating v1 -> v2: skip %s (not bytes)...", iter.Key())
continue
}
migstr := bytes.Replace(domstr, oldIdStr, newIdStr, 1)
if !bytes.Equal(migstr, domstr) {
log.Printf("Migrating v1 -> v2: %s...", iter.Key())
err = s.db.Put(iter.Key(), migstr, nil)
err = s.db.Put(iter.Key(), migstr)
if err != nil {
return
}
@ -157,22 +165,27 @@ func migrateFrom1_domains(s *LevelDBStorage, oldUserId int64, newUserId string)
return
}
func migrateFrom1_provider(s *LevelDBStorage, oldUserId int64, newUserId string) (err error) {
func migrateFrom1_provider(s *KVStorage, oldUserId int64, newUserId string) (err error) {
oldIdStr := []byte(fmt.Sprintf("\"_ownerid\":%d", oldUserId))
newIdStr := []byte(fmt.Sprintf("\"_ownerid\":\"%s\"", newUserId))
iter := s.search("provider-")
iter := s.db.Search("provider-")
defer iter.Release()
for iter.Next() {
domstr := iter.Value()
provRaw := iter.Value()
provstr, ok := provRaw.([]byte)
if !ok {
log.Printf("Migrating v1 -> v2: skip %s (not bytes)...", iter.Key())
continue
}
migstr := bytes.Replace(domstr, oldIdStr, newIdStr, 1)
migstr := bytes.Replace(provstr, oldIdStr, newIdStr, 1)
if !bytes.Equal(migstr, domstr) {
if !bytes.Equal(migstr, provstr) {
log.Printf("Migrating v1 -> v2: %s...", iter.Key())
err = s.db.Put(iter.Key(), migstr, nil)
err = s.db.Put(iter.Key(), migstr)
if err != nil {
return
}
@ -182,22 +195,27 @@ func migrateFrom1_provider(s *LevelDBStorage, oldUserId int64, newUserId string)
return
}
func migrateFrom1_zone(s *LevelDBStorage, oldUserId int64, newUserId string) (err error) {
func migrateFrom1_zone(s *KVStorage, oldUserId int64, newUserId string) (err error) {
oldIdStr := []byte(fmt.Sprintf("\"id_author\":%d", oldUserId))
newIdStr := []byte(fmt.Sprintf("\"id_author\":\"%s\"", newUserId))
iter := s.search("domain.zone-")
iter := s.db.Search("domain.zone-")
defer iter.Release()
for iter.Next() {
domstr := iter.Value()
zoneRaw := iter.Value()
zonestr, ok := zoneRaw.([]byte)
if !ok {
log.Printf("Migrating v1 -> v2: skip %s (not bytes)...", iter.Key())
continue
}
migstr := bytes.Replace(domstr, oldIdStr, newIdStr, 1)
migstr := bytes.Replace(zonestr, oldIdStr, newIdStr, 1)
if !bytes.Equal(migstr, domstr) {
if !bytes.Equal(migstr, zonestr) {
log.Printf("Migrating v1 -> v2: %s...", iter.Key())
err = s.db.Put(iter.Key(), migstr, nil)
err = s.db.Put(iter.Key(), migstr)
if err != nil {
return
}

View file

@ -24,16 +24,16 @@ package database
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"log"
"time"
"git.happydns.org/happyDomain/model"
"github.com/syndtr/goleveldb/leveldb/errors"
)
func migrateFrom2(s *LevelDBStorage) (err error) {
func migrateFrom2(s *KVStorage) (err error) {
err = migrateFrom2_users_tree(s)
if err != nil {
return
@ -50,16 +50,12 @@ type userV2 struct {
Settings happydns.UserSettings
}
func migrateFrom2_users_tree(s *LevelDBStorage) (err error) {
iter := s.search("user-")
defer iter.Release()
func migrateFrom2_users_tree(s *KVStorage) (err error) {
iter := NewKVIterator[userV2](s.db, s.db.Search("user-"))
defer iter.Close()
for iter.Next() {
var user userV2
err = decodeData(iter.Value(), &user)
if err != nil {
return
}
user := iter.Item()
newId := happydns.Identifier(user.Id)
if len(newId) < happydns.IDENTIFIER_LEN {
@ -79,18 +75,18 @@ func migrateFrom2_users_tree(s *LevelDBStorage) (err error) {
log.Printf("Migrating v2 -> v3: %s to user-%s...", iter.Key(), newId.String())
err = s.put(fmt.Sprintf("user-%s", newId.String()), newUser)
err = s.db.Put(fmt.Sprintf("user-%s", newId.String()), newUser)
if err != nil {
return fmt.Errorf("unable to write %s: %w", iter.Key(), err)
}
err = s.delete(string(iter.Key()))
err = s.db.Delete(iter.Key())
if err != nil {
return fmt.Errorf("unable to delete migrated %s: %w", iter.Key(), err)
}
// Migrate object of the user
err = migrateFrom2_auth(s, user.Id, newId, user)
err = migrateFrom2_auth(s, user.Id, newId, *user)
if err != nil {
return fmt.Errorf("unable to migrate auth user for user-%s (%s): %w", newId.String(), user.Email, err)
}
@ -109,15 +105,16 @@ func migrateFrom2_users_tree(s *LevelDBStorage) (err error) {
return
}
func migrateFrom2_auth(s *LevelDBStorage, oldUserId happydns.HexaString, newId happydns.Identifier, user userV2) (err error) {
func migrateFrom2_auth(s *KVStorage, oldUserId happydns.HexaString, newId happydns.Identifier, user userV2) (err error) {
oldIdStr := []byte(fmt.Sprintf("\"Id\":\"%s\"", base64.StdEncoding.EncodeToString(oldUserId)))
newIdStr := []byte(fmt.Sprintf("\"Id\":\"%s\"", newId.String()))
oldAuthKey := fmt.Sprintf("auth-%x", oldUserId)
usrstr, err := s.db.Get([]byte(oldAuthKey), nil)
var usrstr json.RawMessage
err = s.db.Get(oldAuthKey, &usrstr)
if err != nil {
if err == errors.ErrNotFound {
if errors.Is(err, happydns.ErrNotFound) {
user4auth := &happydns.UserAuth{
Id: newId,
Email: user.Email,
@ -129,28 +126,28 @@ func migrateFrom2_auth(s *LevelDBStorage, oldUserId happydns.HexaString, newId h
log.Printf("Migrating v2 -> v3: auth-%s: %s not found, creating it", newId.String(), oldAuthKey)
return s.put(fmt.Sprintf("auth-%s", newId.String()), user4auth)
return s.db.Put(fmt.Sprintf("auth-%s", newId.String()), user4auth)
}
return fmt.Errorf("unable to find/decode %s: %w", oldAuthKey, err)
}
migstr := bytes.Replace(usrstr, oldIdStr, newIdStr, 1)
migstr := bytes.Replace([]byte(usrstr), oldIdStr, newIdStr, 1)
if !bytes.Equal(migstr, usrstr) {
if !bytes.Equal(migstr, []byte(usrstr)) {
var newauth happydns.UserAuth
err = decodeData(migstr, &newauth)
err = s.db.DecodeData(migstr, &newauth)
if err != nil {
log.Printf("From %s to %s", usrstr, migstr)
return fmt.Errorf("unable to reconstruct a valid auth user: %w", err)
}
err = s.db.Put([]byte(fmt.Sprintf("auth-%s", newId.String())), migstr, nil)
err = s.db.Put(fmt.Sprintf("auth-%s", newId.String()), json.RawMessage(migstr))
if err != nil {
return fmt.Errorf("unable to write auth-%s (from %s): %w", newId.String(), oldAuthKey, err)
}
log.Printf("Migrating v2 -> v3: %s to auth-%s...", oldAuthKey, newId.String())
err = s.delete(oldAuthKey)
err = s.db.Delete(oldAuthKey)
if err != nil {
return fmt.Errorf("unable to delete migrated %s: %w", oldAuthKey, err)
}
@ -163,21 +160,22 @@ type sessionV2 struct {
Id []byte `json:"id"`
}
func migrateFrom2_session(s *LevelDBStorage, oldUserId happydns.HexaString, newUserId string) (err error) {
func migrateFrom2_session(s *KVStorage, oldUserId happydns.HexaString, newUserId string) (err error) {
oldOwnerIdStr := []byte(fmt.Sprintf("\"login\":\"%x\"", oldUserId))
newOwnerIdStr := []byte(fmt.Sprintf("\"login\":\"%s\"", newUserId))
iter := s.search("user.session-")
defer iter.Release()
iter := s.db.Search("user.session-")
kvIter := NewKVIterator[json.RawMessage](s.db, iter)
defer kvIter.Close()
for iter.Next() {
usrstr := iter.Value()
for kvIter.Next() {
usrstr := []byte(*kvIter.Item())
if bytes.Contains(usrstr, oldOwnerIdStr) {
var session sessionV2
err = decodeData(usrstr, &session)
err = s.db.DecodeData(usrstr, &session)
if err != nil {
return fmt.Errorf("unable to decode %s: %w", iter.Key(), err)
return fmt.Errorf("unable to decode %s: %w", kvIter.Key(), err)
}
newId := happydns.Identifier(session.Id)
@ -189,15 +187,15 @@ func migrateFrom2_session(s *LevelDBStorage, oldUserId happydns.HexaString, newU
migstr = bytes.Replace(migstr, oldOwnerIdStr, newOwnerIdStr, 1)
if !bytes.Equal(migstr, usrstr) {
err = s.db.Put([]byte(fmt.Sprintf("user.session-%s", newUserId)), migstr, nil)
err = s.db.Put(fmt.Sprintf("user.session-%s", newUserId), json.RawMessage(migstr))
if err != nil {
return fmt.Errorf("unable to write user.session-%s (from %s): %w", newId.String(), iter.Key(), err)
return fmt.Errorf("unable to write user.session-%s (from %s): %w", newId.String(), kvIter.Key(), err)
}
log.Printf("Migrating v2 -> v3: %s to user.session-%s...", iter.Key(), newId.String())
log.Printf("Migrating v2 -> v3: %s to user.session-%s...", kvIter.Key(), newId.String())
err = s.delete(string(iter.Key()))
err = s.db.Delete(kvIter.Key())
if err != nil {
return fmt.Errorf("unable to delete migrated %s: %w", iter.Key(), err)
return fmt.Errorf("unable to delete migrated %s: %w", kvIter.Key(), err)
}
}
}
@ -211,27 +209,28 @@ type providerV2 struct {
OwnerId happydns.HexaString `json:"_ownerid"`
}
func migrateFrom2_provider(s *LevelDBStorage, oldUserId happydns.HexaString, newUserId string) (err error) {
func migrateFrom2_provider(s *KVStorage, oldUserId happydns.HexaString, newUserId string) (err error) {
oldOwnerIdStr := []byte(fmt.Sprintf("\"_ownerid\":\"%x\"", oldUserId))
newOwnerIdStr := []byte(fmt.Sprintf("\"_ownerid\":\"%s\"", newUserId))
iter := s.search("provider-")
defer iter.Release()
iter := s.db.Search("provider-")
kvIter := NewKVIterator[json.RawMessage](s.db, iter)
defer kvIter.Close()
for iter.Next() {
domstr := iter.Value()
for kvIter.Next() {
domstr := []byte(*kvIter.Item())
if bytes.Contains(domstr, oldOwnerIdStr) {
var provider providerV2
err = decodeData(domstr, &provider)
err = s.db.DecodeData(domstr, &provider)
if err != nil {
return fmt.Errorf("unable to decode %s: %w", iter.Key(), err)
return fmt.Errorf("unable to decode %s: %w", kvIter.Key(), err)
}
var newId happydns.Identifier
newId, err = happydns.NewRandomIdentifier()
if err != nil {
return fmt.Errorf("unable to generate a new identifier for %s: %w", iter.Key(), err)
return fmt.Errorf("unable to generate a new identifier for %s: %w", kvIter.Key(), err)
}
oldIdStr := []byte(fmt.Sprintf("\"_id\":%d", provider.Id))
@ -242,20 +241,20 @@ func migrateFrom2_provider(s *LevelDBStorage, oldUserId happydns.HexaString, new
if !bytes.Equal(migstr, domstr) {
var newprv happydns.ProviderMeta
err = decodeData(migstr, &newprv)
err = s.db.DecodeData(migstr, &newprv)
if err != nil {
log.Printf("From %s to %s", domstr, migstr)
return fmt.Errorf("unable to reconstruct a valid provider: %w", err)
}
log.Printf("Migrating v2 -> v3: %s...", iter.Key())
log.Printf("Migrating v2 -> v3: %s...", kvIter.Key())
err = s.db.Put([]byte(fmt.Sprintf("provider-%s", newId.String())), migstr, nil)
err = s.db.Put(fmt.Sprintf("provider-%s", newId.String()), json.RawMessage(migstr))
if err != nil {
return
}
err = s.delete(string(iter.Key()))
err = s.db.Delete(kvIter.Key())
if err != nil {
return
}
@ -277,29 +276,30 @@ type domainV2 struct {
ZoneHistory []int64 `json:"zone_history"`
}
func migrateFrom2_domains(s *LevelDBStorage, oldUserId happydns.HexaString, newUserId string, oldProviderId int64, newProviderId string) (err error) {
func migrateFrom2_domains(s *KVStorage, oldUserId happydns.HexaString, newUserId string, oldProviderId int64, newProviderId string) (err error) {
oldProviderIdStr := []byte(fmt.Sprintf("\"id_provider\":%d", oldProviderId))
newProviderIdStr := []byte(fmt.Sprintf("\"id_provider\":\"%s\"", newProviderId))
oldOwnerIdStr := []byte(fmt.Sprintf("\"id_owner\":\"%x\"", oldUserId))
newOwnerIdStr := []byte(fmt.Sprintf("\"id_owner\":\"%s\"", newUserId))
iter := s.search("domain-")
defer iter.Release()
iter := s.db.Search("domain-")
kvIter := NewKVIterator[json.RawMessage](s.db, iter)
defer kvIter.Close()
for iter.Next() {
domstr := iter.Value()
for kvIter.Next() {
domstr := []byte(*kvIter.Item())
if bytes.Contains(domstr, oldProviderIdStr) && bytes.Contains(domstr, oldOwnerIdStr) {
var domain domainV2
err = decodeData(domstr, &domain)
err = s.db.DecodeData(domstr, &domain)
if err != nil {
return fmt.Errorf("unable to decode %s: %w", iter.Key(), err)
return fmt.Errorf("unable to decode %s: %w", kvIter.Key(), err)
}
var newId happydns.Identifier
newId, err = happydns.NewRandomIdentifier()
if err != nil {
return fmt.Errorf("unable to generate a new identifier for %s: %w", iter.Key(), err)
return fmt.Errorf("unable to generate a new identifier for %s: %w", kvIter.Key(), err)
}
oldIdStr := []byte(fmt.Sprintf("\"id\":%d", domain.Id))
@ -312,7 +312,7 @@ func migrateFrom2_domains(s *LevelDBStorage, oldUserId happydns.HexaString, newU
var newZoneId happydns.Identifier
newZoneId, err = happydns.NewRandomIdentifier()
if err != nil {
return fmt.Errorf("unable to generate a new identifier for a zone of %s: %w", iter.Key(), err)
return fmt.Errorf("unable to generate a new identifier for a zone of %s: %w", kvIter.Key(), err)
}
err = migrateFrom2_zone(s, oldUserId, newUserId, zoneid, newZoneId.String())
@ -333,20 +333,20 @@ func migrateFrom2_domains(s *LevelDBStorage, oldUserId happydns.HexaString, newU
if !bytes.Equal(migstr, domstr) {
var newdn happydns.Domain
err = decodeData(migstr, &newdn)
err = s.db.DecodeData(migstr, &newdn)
if err != nil {
log.Printf("From %s to %s", domstr, migstr)
return fmt.Errorf("unable to reconstruct a valid domain: %w", err)
}
log.Printf("Migrating v2 -> v3: %s...", iter.Key())
log.Printf("Migrating v2 -> v3: %s...", kvIter.Key())
err = s.db.Put([]byte(fmt.Sprintf("domain-%s", newId.String())), migstr, nil)
err = s.db.Put(fmt.Sprintf("domain-%s", newId.String()), json.RawMessage(migstr))
if err != nil {
return
}
err = s.delete(string(iter.Key()))
err = s.db.Delete(kvIter.Key())
if err != nil {
return
}
@ -357,7 +357,7 @@ func migrateFrom2_domains(s *LevelDBStorage, oldUserId happydns.HexaString, newU
return
}
func migrateFrom2_zone(s *LevelDBStorage, oldUserId happydns.HexaString, newUserId string, oldZoneId int64, newZoneId string) (err error) {
func migrateFrom2_zone(s *KVStorage, oldUserId happydns.HexaString, newUserId string, oldZoneId int64, newZoneId string) (err error) {
oldIdStr := []byte(fmt.Sprintf("\"id\":%d", oldZoneId))
newIdStr := []byte(fmt.Sprintf("\"id\":\"%s\"", newZoneId))
oldIdOwnerStr := []byte(fmt.Sprintf("\"id_author\":%d", oldUserId))
@ -365,22 +365,23 @@ func migrateFrom2_zone(s *LevelDBStorage, oldUserId happydns.HexaString, newUser
oldZoneKey := fmt.Sprintf("domain.zone-%d", oldZoneId)
zonestr, err := s.db.Get([]byte(oldZoneKey), nil)
var zonestr json.RawMessage
err = s.db.Get(oldZoneKey, &zonestr)
if err != nil {
return fmt.Errorf("unable to find/decode %s: %w", oldZoneKey, err)
}
migstr := bytes.Replace(zonestr, oldIdStr, newIdStr, 1)
migstr := bytes.Replace([]byte(zonestr), oldIdStr, newIdStr, 1)
migstr = bytes.Replace(migstr, oldIdOwnerStr, newIdOwnerStr, 1)
if !bytes.Equal(migstr, zonestr) {
err = s.db.Put([]byte(fmt.Sprintf("domain.zone-%s", newZoneId)), migstr, nil)
if !bytes.Equal(migstr, []byte(zonestr)) {
err = s.db.Put(fmt.Sprintf("domain.zone-%s", newZoneId), json.RawMessage(migstr))
if err != nil {
return fmt.Errorf("unable to write domain.zone-%s (from %s): %w", newZoneId, oldZoneKey, err)
}
log.Printf("Migrating v2 -> v3: %s to domain.zone-%s...", oldZoneKey, newZoneId)
err = s.delete(oldZoneKey)
err = s.db.Delete(oldZoneKey)
if err != nil {
return fmt.Errorf("unable to delete migrated %s: %w", oldZoneKey, err)
}

View file

@ -27,41 +27,41 @@ import (
"log"
)
func migrateFrom3(s *LevelDBStorage) (err error) {
err = migrateFrom3_records(s)
if err != nil {
return
}
return
func migrateFrom3(s *KVStorage) error {
return migrateFrom3_records(s)
}
func migrateFrom3_records(s *LevelDBStorage) (err error) {
func migrateFrom3_records(s *KVStorage) error {
TypeStr := []byte("\"_svctype\":\"abstract.Origin\"")
iter := s.search("domain.zone-")
iter := NewKVIterator[[]byte](s.db, s.db.Search("domain.zone-"))
defer iter.Close()
for iter.Next() {
zonestr, err := s.db.Get(iter.Key(), nil)
if err != nil {
return fmt.Errorf("unable to find/decode %s: %w", iter.Key(), err)
zonestr, ok := iter.Raw().([]byte)
if !ok {
continue
}
key := iter.Key()
if bytes.Contains(zonestr, TypeStr) {
migstr := zonestr
migstr := make([]byte, len(zonestr))
copy(migstr, zonestr)
migstr = bytes.Replace(migstr, []byte("000000000,\"retry\":"), []byte(",\"retry\":"), 1)
migstr = bytes.Replace(migstr, []byte("000000000,\"expire\":"), []byte(",\"expire\":"), 1)
migstr = bytes.Replace(migstr, []byte("000000000,\"nxttl\":"), []byte(",\"nxttl\":"), 1)
migstr = bytes.Replace(migstr, []byte("000000000,\"ns\":"), []byte(",\"ns\":"), 1)
if !bytes.Equal(migstr, zonestr) {
err = s.db.Put(iter.Key(), migstr, nil)
err := s.db.Put(key, migstr)
if err != nil {
return fmt.Errorf("unable to write %s: %w", iter.Key(), err)
return fmt.Errorf("unable to write %s: %w", key, err)
}
log.Printf("Migrating v2 -> v3: %s (contains Origin)...", iter.Key())
log.Printf("Migrating v3 -> v4: %s (contains Origin)...", key)
}
}
}
return
return nil
}

View file

@ -25,6 +25,6 @@ import (
"fmt"
)
func migrateFrom4(s *LevelDBStorage) (err error) {
func migrateFrom4(s *KVStorage) (err error) {
return fmt.Errorf("Unable to migrate from DB version 4. Please use a previous happyDomain release to perform this migration")
}

View file

@ -25,6 +25,6 @@ import (
"fmt"
)
func migrateFrom5(s *LevelDBStorage) (err error) {
func migrateFrom5(s *KVStorage) (err error) {
return fmt.Errorf("Unable to migrate from DB version 4. Please use a previous happyDomain release to perform this migration")
}

View file

@ -25,7 +25,7 @@ import (
"log"
)
func migrateFrom6(s *LevelDBStorage) error {
func migrateFrom6(s *KVStorage) error {
log.Println("Drop all sessions to use new format")
return s.ClearSessions()
}

View file

@ -191,7 +191,7 @@ func explodeAbstractEMail(dn happydns.Subdomain, in *happydns.ServiceMessage) ([
var migrateFrom7SvcType map[string]func(json.RawMessage) (json.RawMessage, error)
func migrateFrom7(s *LevelDBStorage) (err error) {
func migrateFrom7(s *KVStorage) (err error) {
migrateFrom7SvcType = make(map[string]func(json.RawMessage) (json.RawMessage, error))
// abstract.ACMEChallenge

View file

@ -1,5 +1,5 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2024 happyDomain
// Copyright (c) 2020-2025 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
@ -26,9 +26,9 @@ import (
"log"
)
type LevelDBMigrationFunc func(s *LevelDBStorage) error
type KVMigrationFunc func(s *KVStorage) error
var migrations []LevelDBMigrationFunc = []LevelDBMigrationFunc{
var migrations []KVMigrationFunc = []KVMigrationFunc{
migrateFrom0,
migrateFrom1,
migrateFrom2,
@ -39,49 +39,53 @@ var migrations []LevelDBMigrationFunc = []LevelDBMigrationFunc{
migrateFrom7,
}
func (s *LevelDBStorage) SchemaVersion() int {
type Version struct {
Version int `json:"version"`
}
func (s *KVStorage) SchemaVersion() int {
return len(migrations)
}
func (s *LevelDBStorage) MigrateSchema() (err error) {
func (s *KVStorage) MigrateSchema() (err error) {
found := false
found, err = s.db.Has([]byte("version"), nil)
found, err = s.db.Has("version")
if err != nil {
return
}
var version int
var version Version
if !found {
version = len(migrations)
err = s.put("version", version)
version.Version = len(migrations)
err = s.db.Put("version", version)
if err != nil {
return
}
}
err = s.get("version", &version)
err = s.db.Get("version", &version.Version)
if err != nil {
return
}
if version > len(migrations) {
return fmt.Errorf("Your database has revision %d, which is newer than the revision this happyDomain version can handle (max DB revision %d). Please update happyDomain", version, len(migrations))
if version.Version > len(migrations) {
return fmt.Errorf("Your database has revision %d, which is newer than the revision this happyDomain version can handle (max DB revision %d). Please update happyDomain", version.Version, len(migrations))
}
for v, migration := range migrations[version:] {
log.Printf("Doing migration from %d to %d", version+v, version+v+1)
for v, migration := range migrations[version.Version:] {
log.Printf("Doing migration from %d to %d", version.Version+v, version.Version+v+1)
// Do the migration
if err = migration(s); err != nil {
return
}
// Save the step
if err = s.put("version", version+v+1); err != nil {
if err = s.db.Put("version", version.Version+v+1); err != nil {
return
}
log.Printf("Migration from %d to %d DONE!", version+v, version+v+1)
log.Printf("Migration from %d to %d DONE!", version.Version+v, version.Version+v+1)
}
return nil

View file

@ -1,5 +1,5 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2024 happyDomain
// Copyright (c) 2020-2025 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
@ -25,35 +25,33 @@ import (
"errors"
"fmt"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/util"
"git.happydns.org/happyDomain/model"
)
func (s *LevelDBStorage) ListAllUsers() (happydns.Iterator[happydns.User], error) {
iter := s.search("user-")
return NewLevelDBIterator[happydns.User](s.db, iter), nil
func (s *KVStorage) ListAllUsers() (happydns.Iterator[happydns.User], error) {
iter := s.db.Search("user-")
return NewKVIterator[happydns.User](s.db, iter), nil
}
func (s *LevelDBStorage) getUser(key string) (*happydns.User, error) {
func (s *KVStorage) getUser(key string) (*happydns.User, error) {
u := &happydns.User{}
err := s.get(key, &u)
if errors.Is(err, leveldb.ErrNotFound) {
err := s.db.Get(key, &u)
if errors.Is(err, happydns.ErrNotFound) {
return nil, happydns.ErrUserNotFound
}
return u, err
}
func (s *LevelDBStorage) GetUser(id happydns.Identifier) (u *happydns.User, err error) {
func (s *KVStorage) GetUser(id happydns.Identifier) (u *happydns.User, err error) {
return s.getUser(fmt.Sprintf("user-%s", id.String()))
}
func (s *LevelDBStorage) GetUserByEmail(email string) (*happydns.User, error) {
func (s *KVStorage) GetUserByEmail(email string) (*happydns.User, error) {
users, err := s.ListAllUsers()
if err != nil {
return nil, err
}
defer users.Close()
for users.Next() {
user := users.Item()
@ -65,11 +63,12 @@ func (s *LevelDBStorage) GetUserByEmail(email string) (*happydns.User, error) {
return nil, happydns.ErrUserNotFound
}
func (s *LevelDBStorage) UserExists(email string) bool {
func (s *KVStorage) UserExists(email string) bool {
users, err := s.ListAllUsers()
if err != nil {
return false
}
defer users.Close()
for users.Next() {
if users.Item().Email == email {
@ -80,9 +79,9 @@ func (s *LevelDBStorage) UserExists(email string) bool {
return false
}
func (s *LevelDBStorage) CreateOrUpdateUser(u *happydns.User) error {
func (s *KVStorage) CreateOrUpdateUser(u *happydns.User) error {
if u.Id.IsEmpty() {
_, id, err := s.findIdentifierKey("user-")
_, id, err := s.db.FindIdentifierKey("user-")
if err != nil {
return err
}
@ -90,39 +89,30 @@ func (s *LevelDBStorage) CreateOrUpdateUser(u *happydns.User) error {
u.Id = id
}
return s.put(fmt.Sprintf("user-%s", u.Id.String()), u)
return s.db.Put(fmt.Sprintf("user-%s", u.Id.String()), u)
}
func (s *LevelDBStorage) DeleteUser(uId happydns.Identifier) error {
return s.delete(fmt.Sprintf("user-%s", uId.String()))
func (s *KVStorage) DeleteUser(uId happydns.Identifier) error {
return s.db.Delete(fmt.Sprintf("user-%s", uId.String()))
}
func (s *LevelDBStorage) ClearUsers() error {
func (s *KVStorage) ClearUsers() error {
if err := s.ClearSessions(); err != nil {
return err
}
tx, err := s.db.OpenTransaction()
iter, err := s.ListAllUsers()
if err != nil {
return err
}
iter := tx.NewIterator(util.BytesPrefix([]byte("user-")), nil)
defer iter.Release()
defer iter.Close()
for iter.Next() {
err = tx.Delete(iter.Key(), nil)
err = s.db.Delete(iter.Key())
if err != nil {
tx.Discard()
return err
}
}
err = tx.Commit()
if err != nil {
tx.Discard()
return err
}
return nil
}

View file

@ -0,0 +1,96 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2025 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package database
import (
"errors"
"fmt"
"git.happydns.org/happyDomain/model"
)
func (s *KVStorage) ListAllZones() (happydns.Iterator[happydns.ZoneMessage], error) {
iter := s.db.Search("domain.zone-")
return NewKVIterator[happydns.ZoneMessage](s.db, iter), nil
}
func (s *KVStorage) GetZone(id happydns.Identifier) (*happydns.ZoneMessage, error) {
z := &happydns.ZoneMessage{}
err := s.db.Get(fmt.Sprintf("domain.zone-%s", id.String()), &z)
if errors.Is(err, happydns.ErrNotFound) {
return nil, happydns.ErrZoneNotFound
}
return z, err
}
func (s *KVStorage) getZoneMeta(id string) (z *happydns.ZoneMeta, err error) {
z = &happydns.ZoneMeta{}
err = s.db.Get(id, z)
if errors.Is(err, happydns.ErrNotFound) {
return nil, happydns.ErrZoneNotFound
}
return
}
func (s *KVStorage) GetZoneMeta(id happydns.Identifier) (z *happydns.ZoneMeta, err error) {
z, err = s.getZoneMeta(fmt.Sprintf("domain.zone-%s", id.String()))
return
}
func (s *KVStorage) CreateZone(z *happydns.Zone) error {
key, id, err := s.db.FindIdentifierKey("domain.zone-")
if err != nil {
return err
}
z.Id = id
return s.db.Put(key, z)
}
func (s *KVStorage) UpdateZone(z *happydns.Zone) error {
return s.db.Put(fmt.Sprintf("domain.zone-%s", z.Id.String()), z)
}
func (s *KVStorage) UpdateZoneMessage(z *happydns.ZoneMessage) error {
return s.db.Put(fmt.Sprintf("domain.zone-%s", z.Id.String()), z)
}
func (s *KVStorage) DeleteZone(id happydns.Identifier) error {
return s.db.Delete(fmt.Sprintf("domain.zone-%s", id.String()))
}
func (s *KVStorage) ClearZones() error {
iter, err := s.ListAllZones()
if err != nil {
return err
}
defer iter.Close()
for iter.Next() {
err = s.db.Delete(iter.Key())
if err != nil {
return err
}
}
return nil
}

View file

@ -25,6 +25,7 @@ import (
"flag"
"git.happydns.org/happyDomain/internal/storage"
kv "git.happydns.org/happyDomain/internal/storage/kvtpl"
)
var path string
@ -36,5 +37,10 @@ func init() {
}
func Instantiate() (storage.Storage, error) {
return NewLevelDBStorage(path)
db, err := NewLevelDBStorage(path)
if err != nil {
return nil, err
}
return kv.NewKVDatabase(db)
}

View file

@ -23,14 +23,15 @@ package database // import "git.happydns.org/happyDomain/internal/storage/leveld
import (
"encoding/json"
goerrors "errors"
"fmt"
"log"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/errors"
"github.com/syndtr/goleveldb/leveldb/iterator"
"github.com/syndtr/goleveldb/leveldb/util"
"git.happydns.org/happyDomain/internal/storage"
"git.happydns.org/happyDomain/model"
)
@ -68,16 +69,31 @@ func decodeData(data []byte, v interface{}) error {
return json.Unmarshal(data, v)
}
func (s *LevelDBStorage) get(key string, v interface{}) error {
func (s *LevelDBStorage) DecodeData(data interface{}, v interface{}) error {
b, ok := data.([]byte)
if !ok {
return fmt.Errorf("data to decode are not in []byte format (%T)", data)
}
return decodeData(b, v)
}
func (s *LevelDBStorage) Has(key string) (bool, error) {
return s.db.Has([]byte(key), nil)
}
func (s *LevelDBStorage) Get(key string, v interface{}) error {
data, err := s.db.Get([]byte(key), nil)
if err != nil {
if goerrors.Is(err, leveldb.ErrNotFound) {
return happydns.ErrNotFound
}
return err
}
return decodeData(data, v)
}
func (s *LevelDBStorage) put(key string, v interface{}) error {
func (s *LevelDBStorage) Put(key string, v interface{}) error {
data, err := json.Marshal(v)
if err != nil {
return err
@ -86,7 +102,7 @@ func (s *LevelDBStorage) put(key string, v interface{}) error {
return s.db.Put([]byte(key), data, nil)
}
func (s *LevelDBStorage) findIdentifierKey(prefix string) (key string, id happydns.Identifier, err error) {
func (s *LevelDBStorage) FindIdentifierKey(prefix string) (key string, id happydns.Identifier, err error) {
found := true
for found {
id, err = happydns.NewRandomIdentifier()
@ -103,10 +119,10 @@ func (s *LevelDBStorage) findIdentifierKey(prefix string) (key string, id happyd
return
}
func (s *LevelDBStorage) delete(key string) error {
func (s *LevelDBStorage) Delete(key string) error {
return s.db.Delete([]byte(key), nil)
}
func (s *LevelDBStorage) search(prefix string) iterator.Iterator {
return s.db.NewIterator(util.BytesPrefix([]byte(prefix)), nil)
func (s *LevelDBStorage) Search(prefix string) storage.Iterator {
return NewIterator(s.db.NewIterator(util.BytesPrefix([]byte(prefix)), nil))
}

View file

@ -1,107 +0,0 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2024 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package database
import (
"errors"
"fmt"
"strings"
"github.com/syndtr/goleveldb/leveldb"
"git.happydns.org/happyDomain/model"
)
func (s *LevelDBStorage) ListAllDomainLogs() (happydns.Iterator[happydns.DomainLogWithDomainId], error) {
iter := s.search("domain.log|")
return NewLevelDBIteratorCustomDecode[happydns.DomainLogWithDomainId](s.db, iter, func(data []byte, v interface{}) error {
err := decodeData(data, v)
if err != nil {
return err
}
st := strings.Split(string(iter.Key()), "|")
if len(st) < 3 {
return fmt.Errorf("invalid domain log key: %s", string(iter.Key()))
}
v.(*happydns.DomainLogWithDomainId).DomainId, err = happydns.NewIdentifierFromString(st[1])
return err
}), nil
}
func (s *LevelDBStorage) ListDomainLogs(domain *happydns.Domain) (logs []*happydns.DomainLog, err error) {
iter := s.search(fmt.Sprintf("domain.log|%s|", domain.Id.String()))
defer iter.Release()
for iter.Next() {
var z happydns.DomainLog
err = decodeData(iter.Value(), &z)
if err != nil {
return
}
logs = append(logs, &z)
}
return
}
func (s *LevelDBStorage) getDomainLog(id string) (l *happydns.DomainLog, d *happydns.Domain, err error) {
l = &happydns.DomainLog{}
err = s.get(id, l)
if errors.Is(err, leveldb.ErrNotFound) {
return nil, nil, happydns.ErrDomainLogNotFound
}
st := strings.Split(id, "|")
if len(st) < 3 {
return
}
d = &happydns.Domain{}
err = s.get(id, fmt.Sprintf("domain-%s", st[1]))
if errors.Is(err, leveldb.ErrNotFound) {
return nil, nil, happydns.ErrDomainNotFound
}
return
}
func (s *LevelDBStorage) CreateDomainLog(d *happydns.Domain, l *happydns.DomainLog) error {
key, id, err := s.findIdentifierKey(fmt.Sprintf("domain.log|%s|", d.Id.String()))
if err != nil {
return err
}
l.Id = id
return s.put(key, l)
}
func (s *LevelDBStorage) UpdateDomainLog(d *happydns.Domain, l *happydns.DomainLog) error {
return s.put(fmt.Sprintf("domain.log|%s|%s", d.Id.String(), l.Id.String()), l)
}
func (s *LevelDBStorage) DeleteDomainLog(d *happydns.Domain, l *happydns.DomainLog) error {
return s.delete(fmt.Sprintf("domain.log|%s|%s", d.Id.String(), l.Id.String()))
}

View file

@ -22,104 +22,35 @@
package database
import (
"fmt"
"log"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/iterator"
)
// LevelDBIterator is a generic implementation of Iterator for LevelDB.
type LevelDBIterator[T any] struct {
db *leveldb.DB
iter iterator.Iterator
err error
item *T
decode func([]byte, interface{}) error
type LevelDBIterator struct {
iter iterator.Iterator
}
// NewLevelDBIterator creates a new LevelDBIterator instance for the given LevelDB iterator and decode function.
func NewLevelDBIterator[T any](db *leveldb.DB, iter iterator.Iterator) *LevelDBIterator[T] {
return &LevelDBIterator[T]{
db: db,
iter: iter,
decode: decodeData,
func NewIterator(iter iterator.Iterator) *LevelDBIterator {
return &LevelDBIterator{
iter: iter,
}
}
// NewLevelDBIterator creates a new LevelDBIterator instance for the given LevelDB iterator and decode function.
func NewLevelDBIteratorCustomDecode[T any](db *leveldb.DB, iter iterator.Iterator, decodeFunc func([]byte, interface{}) error) *LevelDBIterator[T] {
return &LevelDBIterator[T]{
db: db,
iter: iter,
decode: decodeFunc,
}
func (it *LevelDBIterator) Next() bool {
return it.iter.Next()
}
// Next moves the iterator to the next valid item.
// Skips items that fail to decode and logs the error.
func (it *LevelDBIterator[T]) Next() bool {
for it.iter.Next() {
var value T
err := it.decode(it.iter.Value(), &value)
if err != nil {
log.Printf("LevelDBIterator: error decoding item at key %q: %s", it.iter.Key(), err)
it.err = err
continue
}
it.item = &value
return true
}
return false
func (it *LevelDBIterator) Key() string {
return string(it.iter.Key())
}
// NextWithError advances the iterator to the next item, on decode error it doesn't continue to the next item.
// Returns true if there is a next item, false otherwise.
func (it *LevelDBIterator[T]) NextWithError() bool {
if it.iter.Next() {
var value T
err := it.decode(it.iter.Value(), &value)
if err != nil {
it.err = err
it.item = nil
} else {
it.err = nil
it.item = &value
}
return true
}
return false
}
// Item returns the current item from the iterator.
// Only valid after a successful call to Next().
func (it *LevelDBIterator[T]) Item() *T {
return it.item
}
// DropItem deletes the key currently pointed to by the iterator.
func (it *LevelDBIterator[T]) DropItem() error {
if it.iter == nil || !it.iter.Valid() {
return fmt.Errorf("DropItem: iterator is not valid")
}
return it.db.Delete(it.iter.Key(), nil)
}
// Raw returns the raw (non-decoded) value at the current iterator position.
// Should only be called after a successful call to Next().
func (it *LevelDBIterator[T]) Raw() []byte {
if it.iter == nil || !it.iter.Valid() {
return []byte{}
}
func (it *LevelDBIterator) Value() interface{} {
return it.iter.Value()
}
// Err returns the first error encountered during iteration, if any.
func (it *LevelDBIterator[T]) Err() error {
return it.err
func (it *LevelDBIterator) Valid() bool {
return it.iter.Valid()
}
// Close releases resources held by the underlying LevelDB iterator.
func (it *LevelDBIterator[T]) Close() {
func (it *LevelDBIterator) Release() {
it.iter.Release()
}

View file

@ -1,138 +0,0 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2024 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package database
import (
"bytes"
"errors"
"fmt"
"git.happydns.org/happyDomain/model"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/util"
)
func (s *LevelDBStorage) ListAllProviders() (happydns.Iterator[happydns.ProviderMessage], error) {
iter := s.search("provider-")
return NewLevelDBIterator[happydns.ProviderMessage](s.db, iter), nil
}
func (s *LevelDBStorage) getProviderMeta(id happydns.Identifier) (*happydns.ProviderMessage, error) {
v, err := s.db.Get(id, nil)
if err != nil {
if errors.Is(err, leveldb.ErrNotFound) {
return nil, happydns.ErrProviderNotFound
} else {
return nil, err
}
}
srcMsg := &happydns.ProviderMessage{}
err = decodeData(v, srcMsg)
return srcMsg, err
}
func (s *LevelDBStorage) ListProviders(u *happydns.User) (srcs happydns.ProviderMessages, err error) {
iter := s.search("provider-")
defer iter.Release()
for iter.Next() {
var srcMsg happydns.ProviderMessage
err = decodeData(iter.Value(), &srcMsg)
if err != nil {
return
}
if !bytes.Equal(srcMsg.Owner, u.Id) {
continue
}
srcs = append(srcs, &srcMsg)
}
return
}
func (s *LevelDBStorage) GetProvider(id happydns.Identifier) (*happydns.ProviderMessage, error) {
v, err := s.db.Get([]byte(fmt.Sprintf("provider-%s", id.String())), nil)
if err != nil {
if errors.Is(err, leveldb.ErrNotFound) {
return nil, happydns.ErrProviderNotFound
} else {
return nil, err
}
}
var prvdMsg happydns.ProviderMessage
err = decodeData(v, &prvdMsg)
if err != nil {
return nil, err
}
return &prvdMsg, err
}
func (s *LevelDBStorage) CreateProvider(prvd *happydns.Provider) error {
key, id, err := s.findIdentifierKey("provider-")
if err != nil {
return err
}
prvd.Id = id
return s.put(key, prvd)
}
func (s *LevelDBStorage) UpdateProvider(prvd *happydns.Provider) error {
return s.put(fmt.Sprintf("provider-%s", prvd.Id.String()), prvd)
}
func (s *LevelDBStorage) DeleteProvider(prvdId happydns.Identifier) error {
return s.delete(fmt.Sprintf("provider-%s", prvdId.String()))
}
func (s *LevelDBStorage) ClearProviders() error {
tx, err := s.db.OpenTransaction()
if err != nil {
return err
}
iter := tx.NewIterator(util.BytesPrefix([]byte("provider-")), nil)
defer iter.Release()
for iter.Next() {
err = tx.Delete(iter.Key(), nil)
if err != nil {
tx.Discard()
return err
}
}
err = tx.Commit()
if err != nil {
tx.Discard()
return err
}
return nil
}

View file

@ -1,122 +0,0 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2024 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package database
import (
"errors"
"fmt"
"git.happydns.org/happyDomain/model"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/util"
)
func (s *LevelDBStorage) ListAllSessions() (happydns.Iterator[happydns.Session], error) {
iter := s.search("user.session-")
return NewLevelDBIterator[happydns.Session](s.db, iter), nil
}
func (s *LevelDBStorage) getSession(id string) (*happydns.Session, error) {
session := &happydns.Session{}
err := s.get(id, &session)
if errors.Is(err, leveldb.ErrNotFound) {
return nil, happydns.ErrSessionNotFound
}
return session, err
}
func (s *LevelDBStorage) GetSession(id string) (session *happydns.Session, err error) {
return s.getSession(fmt.Sprintf("user.session-%s", id))
}
func (s *LevelDBStorage) ListAuthUserSessions(user *happydns.UserAuth) (sessions []*happydns.Session, err error) {
iter := s.search("user.session-")
defer iter.Release()
for iter.Next() {
var s happydns.Session
err = decodeData(iter.Value(), &s)
if err != nil {
return
}
if s.IdUser.Equals(user.Id) {
sessions = append(sessions, &s)
}
}
return
}
func (s *LevelDBStorage) ListUserSessions(userid happydns.Identifier) (sessions []*happydns.Session, err error) {
iter := s.search("user.session-")
defer iter.Release()
for iter.Next() {
var s happydns.Session
err = decodeData(iter.Value(), &s)
if err != nil {
return
}
if s.IdUser.Equals(userid) {
sessions = append(sessions, &s)
}
}
return
}
func (s *LevelDBStorage) UpdateSession(session *happydns.Session) error {
return s.put(fmt.Sprintf("user.session-%s", session.Id), session)
}
func (s *LevelDBStorage) DeleteSession(id string) error {
return s.delete(fmt.Sprintf("user.session-%s", id))
}
func (s *LevelDBStorage) ClearSessions() error {
tx, err := s.db.OpenTransaction()
if err != nil {
return err
}
iter := tx.NewIterator(util.BytesPrefix([]byte("user.session-")), nil)
defer iter.Release()
for iter.Next() {
err = tx.Delete(iter.Key(), nil)
if err != nil {
tx.Discard()
return err
}
}
err = tx.Commit()
if err != nil {
tx.Discard()
return err
}
return nil
}

View file

@ -1,108 +0,0 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2024 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package database
import (
"errors"
"fmt"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/util"
"git.happydns.org/happyDomain/model"
)
func (s *LevelDBStorage) ListAllZones() (happydns.Iterator[happydns.ZoneMessage], error) {
iter := s.search("domain.zone-")
return NewLevelDBIterator[happydns.ZoneMessage](s.db, iter), nil
}
func (s *LevelDBStorage) GetZone(id happydns.Identifier) (*happydns.ZoneMessage, error) {
z := &happydns.ZoneMessage{}
err := s.get(fmt.Sprintf("domain.zone-%s", id.String()), &z)
if errors.Is(err, leveldb.ErrNotFound) {
return nil, happydns.ErrZoneNotFound
}
return z, err
}
func (s *LevelDBStorage) getZoneMeta(id string) (z *happydns.ZoneMeta, err error) {
z = &happydns.ZoneMeta{}
err = s.get(id, z)
if errors.Is(err, leveldb.ErrNotFound) {
return nil, happydns.ErrZoneNotFound
}
return
}
func (s *LevelDBStorage) GetZoneMeta(id happydns.Identifier) (z *happydns.ZoneMeta, err error) {
z, err = s.getZoneMeta(fmt.Sprintf("domain.zone-%s", id.String()))
return
}
func (s *LevelDBStorage) CreateZone(z *happydns.Zone) error {
key, id, err := s.findIdentifierKey("domain.zone-")
if err != nil {
return err
}
z.Id = id
return s.put(key, z)
}
func (s *LevelDBStorage) UpdateZone(z *happydns.Zone) error {
return s.put(fmt.Sprintf("domain.zone-%s", z.Id.String()), z)
}
func (s *LevelDBStorage) UpdateZoneMessage(z *happydns.ZoneMessage) error {
return s.put(fmt.Sprintf("domain.zone-%s", z.Id.String()), z)
}
func (s *LevelDBStorage) DeleteZone(id happydns.Identifier) error {
return s.delete(fmt.Sprintf("domain.zone-%s", id.String()))
}
func (s *LevelDBStorage) ClearZones() error {
tx, err := s.db.OpenTransaction()
if err != nil {
return err
}
iter := tx.NewIterator(util.BytesPrefix([]byte("domain.zone-")), nil)
defer iter.Release()
for iter.Next() {
err = tx.Delete(iter.Key(), nil)
if err != nil {
tx.Discard()
return err
}
}
err = tx.Commit()
if err != nil {
tx.Discard()
return err
}
return nil
}

View file

@ -5,6 +5,7 @@ import (
"time"
"git.happydns.org/happyDomain/internal/storage/inmemory"
kv "git.happydns.org/happyDomain/internal/storage/kvtpl"
"git.happydns.org/happyDomain/internal/usecase"
userUC "git.happydns.org/happyDomain/internal/usecase/user"
"git.happydns.org/happyDomain/model"
@ -22,8 +23,9 @@ func (u testUserInfo) JoinNewsletter() bool { return u.newsletter }
func Test_CompleteAuthentication(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
userUsecase := userUC.NewUserUsecases(mem, nil, nil, nil)
authenticationUsecase := usecase.NewAuthenticationUsecase(&happydns.Options{}, mem, userUsecase)
db, _ := kv.NewKVDatabase(mem)
userUsecase := userUC.NewUserUsecases(db, nil, nil, nil)
authenticationUsecase := usecase.NewAuthenticationUsecase(&happydns.Options{}, db, userUsecase)
uinfo := testUserInfo{
id: happydns.Identifier([]byte("user-123")),
@ -40,7 +42,7 @@ func Test_CompleteAuthentication(t *testing.T) {
}
// Check the user is correctly stored in db
stored, err := mem.GetUser(happydns.Identifier([]byte("user-123")))
stored, err := db.GetUser(happydns.Identifier([]byte("user-123")))
if err != nil {
t.Fatalf("expected stored user, got error: %v", err)
}
@ -61,9 +63,10 @@ func (ds *testNewsletterSubscription) SubscribeToNewsletter(u happydns.UserInfo)
func Test_CompleteAuthentication_WithNewsletter(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
db, _ := kv.NewKVDatabase(mem)
mockNewsletterSubscription := &testNewsletterSubscription{}
userUsecase := userUC.NewUserUsecases(mem, mockNewsletterSubscription, nil, nil)
authenticationUsecase := usecase.NewAuthenticationUsecase(&happydns.Options{}, mem, userUsecase)
userUsecase := userUC.NewUserUsecases(db, mockNewsletterSubscription, nil, nil)
authenticationUsecase := usecase.NewAuthenticationUsecase(&happydns.Options{}, db, userUsecase)
uinfo := testUserInfo{
id: happydns.Identifier([]byte("user-123")),
@ -97,6 +100,7 @@ func Test_CompleteAuthentication_WithNewsletter(t *testing.T) {
func Test_AuthenticateUserWithPassword_WrongPassword(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
db, _ := kv.NewKVDatabase(mem)
authUser := &happydns.UserAuth{
Email: "a@b.c",
@ -106,13 +110,13 @@ func Test_AuthenticateUserWithPassword_WrongPassword(t *testing.T) {
t.Fatalf("unexpected error: %v", err)
}
err = mem.CreateAuthUser(authUser)
err = db.CreateAuthUser(authUser)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
userUsecase := userUC.NewUserUsecases(mem, nil, nil, nil)
authenticationUsecase := usecase.NewAuthenticationUsecase(&happydns.Options{}, mem, userUsecase)
userUsecase := userUC.NewUserUsecases(db, nil, nil, nil)
authenticationUsecase := usecase.NewAuthenticationUsecase(&happydns.Options{}, db, userUsecase)
_, err = authenticationUsecase.AuthenticateUserWithPassword(happydns.LoginRequest{
Email: "a@b.c",
@ -125,6 +129,7 @@ func Test_AuthenticateUserWithPassword_WrongPassword(t *testing.T) {
func Test_AuthenticateUserWithPassword_WeakPassword(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
db, _ := kv.NewKVDatabase(mem)
authUser := &happydns.UserAuth{
Email: "a@b.c",
@ -134,13 +139,13 @@ func Test_AuthenticateUserWithPassword_WeakPassword(t *testing.T) {
t.Fatalf("unexpected error: %v", err)
}
err = mem.CreateAuthUser(authUser)
err = db.CreateAuthUser(authUser)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
userUsecase := userUC.NewUserUsecases(mem, nil, nil, nil)
authenticationUsecase := usecase.NewAuthenticationUsecase(&happydns.Options{}, mem, userUsecase)
userUsecase := userUC.NewUserUsecases(db, nil, nil, nil)
authenticationUsecase := usecase.NewAuthenticationUsecase(&happydns.Options{}, db, userUsecase)
_, err = authenticationUsecase.AuthenticateUserWithPassword(happydns.LoginRequest{
Email: "a@b.c",
@ -153,6 +158,7 @@ func Test_AuthenticateUserWithPassword_WeakPassword(t *testing.T) {
func Test_AuthenticateUserWithPassword_UnverifiedEmail(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
db, _ := kv.NewKVDatabase(mem)
authUser := &happydns.UserAuth{
Email: "a@b.c",
@ -162,13 +168,13 @@ func Test_AuthenticateUserWithPassword_UnverifiedEmail(t *testing.T) {
t.Fatalf("unexpected error: %v", err)
}
err = mem.CreateAuthUser(authUser)
err = db.CreateAuthUser(authUser)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
userUsecase := userUC.NewUserUsecases(mem, nil, nil, nil)
authenticationUsecase := usecase.NewAuthenticationUsecase(&happydns.Options{}, mem, userUsecase)
userUsecase := userUC.NewUserUsecases(db, nil, nil, nil)
authenticationUsecase := usecase.NewAuthenticationUsecase(&happydns.Options{}, db, userUsecase)
_, err = authenticationUsecase.AuthenticateUserWithPassword(happydns.LoginRequest{
Email: "a@b.c",
@ -181,6 +187,7 @@ func Test_AuthenticateUserWithPassword_UnverifiedEmail(t *testing.T) {
func Test_AuthenticateUserWithPassword_NoEmail(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
db, _ := kv.NewKVDatabase(mem)
authUser := &happydns.UserAuth{
Email: "a@b.c",
@ -190,13 +197,13 @@ func Test_AuthenticateUserWithPassword_NoEmail(t *testing.T) {
t.Fatalf("unexpected error: %v", err)
}
err = mem.CreateAuthUser(authUser)
err = db.CreateAuthUser(authUser)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
userUsecase := userUC.NewUserUsecases(mem, nil, nil, nil)
authenticationUsecase := usecase.NewAuthenticationUsecase(&happydns.Options{NoMail: true}, mem, userUsecase)
userUsecase := userUC.NewUserUsecases(db, nil, nil, nil)
authenticationUsecase := usecase.NewAuthenticationUsecase(&happydns.Options{NoMail: true}, db, userUsecase)
_, err = authenticationUsecase.AuthenticateUserWithPassword(happydns.LoginRequest{
Email: "a@b.c",
@ -209,6 +216,7 @@ func Test_AuthenticateUserWithPassword_NoEmail(t *testing.T) {
func Test_AuthenticateUserWithPassword(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
db, _ := kv.NewKVDatabase(mem)
now := time.Now()
authUser := &happydns.UserAuth{
@ -220,13 +228,13 @@ func Test_AuthenticateUserWithPassword(t *testing.T) {
t.Fatalf("unexpected error: %v", err)
}
err = mem.CreateAuthUser(authUser)
err = db.CreateAuthUser(authUser)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
userUsecase := userUC.NewUserUsecases(mem, nil, nil, nil)
authenticationUsecase := usecase.NewAuthenticationUsecase(&happydns.Options{}, mem, userUsecase)
userUsecase := userUC.NewUserUsecases(db, nil, nil, nil)
authenticationUsecase := usecase.NewAuthenticationUsecase(&happydns.Options{}, db, userUsecase)
_, err = authenticationUsecase.AuthenticateUserWithPassword(happydns.LoginRequest{
Email: "a@b.c",

View file

@ -26,7 +26,9 @@ import (
"testing"
"time"
"git.happydns.org/happyDomain/internal/storage"
"git.happydns.org/happyDomain/internal/storage/inmemory"
kv "git.happydns.org/happyDomain/internal/storage/kvtpl"
"git.happydns.org/happyDomain/internal/usecase/authuser"
"git.happydns.org/happyDomain/model"
)
@ -47,8 +49,9 @@ func (m *MockCloseUserSessionsUsecase) ByID(userID happydns.Identifier) error {
return m.CloseAll(&happydns.UserAuth{Id: userID})
}
func setupTestService() (*authuser.Service, *inmemory.InMemoryStorage) {
store, _ := inmemory.NewInMemoryStorage()
func setupTestService() (*authuser.Service, storage.Storage) {
mem, _ := inmemory.NewInMemoryStorage()
store, _ := kv.NewKVDatabase(mem)
cfg := &happydns.Options{
DisableRegistration: false,
}
@ -75,7 +78,8 @@ func TestCanRegister_Success(t *testing.T) {
}
func TestCanRegister_Closed(t *testing.T) {
store, _ := inmemory.NewInMemoryStorage()
mem, _ := inmemory.NewInMemoryStorage()
store, _ := kv.NewKVDatabase(mem)
cfg := &happydns.Options{
DisableRegistration: true, // Registration closed
}
@ -344,7 +348,8 @@ func TestCheckNewPassword(t *testing.T) {
// ========== DeleteAuthUser Tests ==========
func TestDeleteAuthUser(t *testing.T) {
store, _ := inmemory.NewInMemoryStorage()
mem, _ := inmemory.NewInMemoryStorage()
store, _ := kv.NewKVDatabase(mem)
cfg := &happydns.Options{
DisableRegistration: false,
}

View file

@ -25,7 +25,9 @@ import (
"fmt"
"testing"
"git.happydns.org/happyDomain/internal/storage"
"git.happydns.org/happyDomain/internal/storage/inmemory"
kv "git.happydns.org/happyDomain/internal/storage/kvtpl"
"git.happydns.org/happyDomain/internal/usecase/domain"
providerUC "git.happydns.org/happyDomain/internal/usecase/provider"
zoneUC "git.happydns.org/happyDomain/internal/usecase/zone"
@ -90,7 +92,7 @@ func (m *mockDomainLogAppender) AppendDomainLog(d *happydns.Domain, log *happydn
// Helper functions
func createTestUser(t *testing.T, store *inmemory.InMemoryStorage, email string) *happydns.User {
func createTestUser(t *testing.T, store storage.Storage, email string) *happydns.User {
user := &happydns.User{
Id: happydns.Identifier([]byte("user-" + email)),
Email: email,
@ -101,7 +103,7 @@ func createTestUser(t *testing.T, store *inmemory.InMemoryStorage, email string)
return user
}
func createTestProvider(t *testing.T, store *inmemory.InMemoryStorage, user *happydns.User, name string) happydns.Identifier {
func createTestProvider(t *testing.T, store storage.Storage, user *happydns.User, name string) happydns.Identifier {
provider := &happydns.Provider{
ProviderMeta: happydns.ProviderMeta{
Type: "mockProviderBody",
@ -118,7 +120,7 @@ func createTestProvider(t *testing.T, store *inmemory.InMemoryStorage, user *hap
return provider.Id
}
func setupTestService(store *inmemory.InMemoryStorage) (*domain.Service, *mockDomainLogAppender) {
func setupTestService(store storage.Storage) (*domain.Service, *mockDomainLogAppender) {
// Create the provider service
providerService := providerUC.NewService(store)
@ -146,10 +148,11 @@ func setupTestService(store *inmemory.InMemoryStorage) (*domain.Service, *mockDo
func Test_CreateDomain(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
service, logAppender := setupTestService(mem)
db, _ := kv.NewKVDatabase(mem)
service, logAppender := setupTestService(db)
user := createTestUser(t, mem, "test@example.com")
providerId := createTestProvider(t, mem, user, "Test Provider")
user := createTestUser(t, db, "test@example.com")
providerId := createTestProvider(t, db, user, "Test Provider")
domainToCreate := &happydns.Domain{
DomainName: "example.com",
@ -191,9 +194,10 @@ func Test_CreateDomain(t *testing.T) {
func Test_CreateDomain_InvalidProvider(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
service, _ := setupTestService(mem)
db, _ := kv.NewKVDatabase(mem)
service, _ := setupTestService(db)
user := createTestUser(t, mem, "test@example.com")
user := createTestUser(t, db, "test@example.com")
invalidProviderId := happydns.Identifier([]byte("invalid-provider"))
domainToCreate := &happydns.Domain{
@ -209,10 +213,11 @@ func Test_CreateDomain_InvalidProvider(t *testing.T) {
func Test_GetUserDomain(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
service, _ := setupTestService(mem)
db, _ := kv.NewKVDatabase(mem)
service, _ := setupTestService(db)
user := createTestUser(t, mem, "test@example.com")
providerId := createTestProvider(t, mem, user, "Test Provider")
user := createTestUser(t, db, "test@example.com")
providerId := createTestProvider(t, db, user, "Test Provider")
// Create a domain
domainToCreate := &happydns.Domain{
@ -248,11 +253,12 @@ func Test_GetUserDomain(t *testing.T) {
func Test_GetUserDomain_WrongUser(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
service, _ := setupTestService(mem)
db, _ := kv.NewKVDatabase(mem)
service, _ := setupTestService(db)
user1 := createTestUser(t, mem, "user1@example.com")
user2 := createTestUser(t, mem, "user2@example.com")
providerId := createTestProvider(t, mem, user1, "Test Provider")
user1 := createTestUser(t, db, "user1@example.com")
user2 := createTestUser(t, db, "user2@example.com")
providerId := createTestProvider(t, db, user1, "Test Provider")
// Create a domain for user1
domainToCreate := &happydns.Domain{
@ -283,9 +289,10 @@ func Test_GetUserDomain_WrongUser(t *testing.T) {
func Test_GetUserDomain_NotFound(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
service, _ := setupTestService(mem)
db, _ := kv.NewKVDatabase(mem)
service, _ := setupTestService(db)
user := createTestUser(t, mem, "test@example.com")
user := createTestUser(t, db, "test@example.com")
nonexistentId := happydns.Identifier([]byte("nonexistent-domain"))
_, err := service.GetUserDomain(user, nonexistentId)
@ -299,10 +306,11 @@ func Test_GetUserDomain_NotFound(t *testing.T) {
func Test_GetUserDomainByFQDN(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
service, _ := setupTestService(mem)
db, _ := kv.NewKVDatabase(mem)
service, _ := setupTestService(db)
user := createTestUser(t, mem, "test@example.com")
providerId := createTestProvider(t, mem, user, "Test Provider")
user := createTestUser(t, db, "test@example.com")
providerId := createTestProvider(t, db, user, "Test Provider")
// Create a domain
domainToCreate := &happydns.Domain{
@ -331,10 +339,11 @@ func Test_GetUserDomainByFQDN(t *testing.T) {
func Test_ListUserDomains(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
service, _ := setupTestService(mem)
db, _ := kv.NewKVDatabase(mem)
service, _ := setupTestService(db)
user := createTestUser(t, mem, "test@example.com")
providerId := createTestProvider(t, mem, user, "Test Provider")
user := createTestUser(t, db, "test@example.com")
providerId := createTestProvider(t, db, user, "Test Provider")
// Create multiple domains
domainNames := []string{"example1.com", "example2.com", "example3.com"}
@ -362,12 +371,13 @@ func Test_ListUserDomains(t *testing.T) {
func Test_ListUserDomains_MultipleUsers(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
service, _ := setupTestService(mem)
db, _ := kv.NewKVDatabase(mem)
service, _ := setupTestService(db)
user1 := createTestUser(t, mem, "user1@example.com")
user2 := createTestUser(t, mem, "user2@example.com")
providerId1 := createTestProvider(t, mem, user1, "Provider 1")
providerId2 := createTestProvider(t, mem, user2, "Provider 2")
user1 := createTestUser(t, db, "user1@example.com")
user2 := createTestUser(t, db, "user2@example.com")
providerId1 := createTestProvider(t, db, user1, "Provider 1")
providerId2 := createTestProvider(t, db, user2, "Provider 2")
// Create domains for user1
for i := 1; i <= 2; i++ {
@ -412,9 +422,10 @@ func Test_ListUserDomains_MultipleUsers(t *testing.T) {
func Test_ListUserDomains_Empty(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
service, _ := setupTestService(mem)
db, _ := kv.NewKVDatabase(mem)
service, _ := setupTestService(db)
user := createTestUser(t, mem, "test@example.com")
user := createTestUser(t, db, "test@example.com")
// List domains (should be empty)
domains, err := service.ListUserDomains(user)
@ -429,10 +440,11 @@ func Test_ListUserDomains_Empty(t *testing.T) {
func Test_UpdateDomain(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
service, logAppender := setupTestService(mem)
db, _ := kv.NewKVDatabase(mem)
service, logAppender := setupTestService(db)
user := createTestUser(t, mem, "test@example.com")
providerId := createTestProvider(t, mem, user, "Test Provider")
user := createTestUser(t, db, "test@example.com")
providerId := createTestProvider(t, db, user, "Test Provider")
// Create a domain
domainToCreate := &happydns.Domain{
@ -480,10 +492,11 @@ func Test_UpdateDomain(t *testing.T) {
func Test_UpdateDomain_PreventIdChange(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
service, _ := setupTestService(mem)
db, _ := kv.NewKVDatabase(mem)
service, _ := setupTestService(db)
user := createTestUser(t, mem, "test@example.com")
providerId := createTestProvider(t, mem, user, "Test Provider")
user := createTestUser(t, db, "test@example.com")
providerId := createTestProvider(t, db, user, "Test Provider")
// Create a domain
domainToCreate := &happydns.Domain{
@ -521,11 +534,12 @@ func Test_UpdateDomain_PreventIdChange(t *testing.T) {
func Test_UpdateDomain_WrongUser(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
service, _ := setupTestService(mem)
db, _ := kv.NewKVDatabase(mem)
service, _ := setupTestService(db)
user1 := createTestUser(t, mem, "user1@example.com")
user2 := createTestUser(t, mem, "user2@example.com")
providerId := createTestProvider(t, mem, user1, "Test Provider")
user1 := createTestUser(t, db, "user1@example.com")
user2 := createTestUser(t, db, "user2@example.com")
providerId := createTestProvider(t, db, user1, "Test Provider")
// Create a domain for user1
domainToCreate := &happydns.Domain{
@ -555,10 +569,11 @@ func Test_UpdateDomain_WrongUser(t *testing.T) {
func Test_DeleteDomain(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
service, _ := setupTestService(mem)
db, _ := kv.NewKVDatabase(mem)
service, _ := setupTestService(db)
user := createTestUser(t, mem, "test@example.com")
providerId := createTestProvider(t, mem, user, "Test Provider")
user := createTestUser(t, db, "test@example.com")
providerId := createTestProvider(t, db, user, "Test Provider")
// Create a domain
domainToCreate := &happydns.Domain{
@ -595,10 +610,11 @@ func Test_DeleteDomain(t *testing.T) {
func Test_UpdateDomain_Alias(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
service, _ := setupTestService(mem)
db, _ := kv.NewKVDatabase(mem)
service, _ := setupTestService(db)
user := createTestUser(t, mem, "test@example.com")
providerId := createTestProvider(t, mem, user, "Test Provider")
user := createTestUser(t, db, "test@example.com")
providerId := createTestProvider(t, db, user, "Test Provider")
// Create a domain
domainToCreate := &happydns.Domain{

View file

@ -25,12 +25,14 @@ import (
"testing"
"time"
"git.happydns.org/happyDomain/internal/storage"
"git.happydns.org/happyDomain/internal/storage/inmemory"
kv "git.happydns.org/happyDomain/internal/storage/kvtpl"
"git.happydns.org/happyDomain/internal/usecase/domain_log"
"git.happydns.org/happyDomain/model"
)
func createTestUser(t *testing.T, store *inmemory.InMemoryStorage, email string) *happydns.User {
func createTestUser(t *testing.T, store storage.Storage, email string) *happydns.User {
user := &happydns.User{
Id: happydns.Identifier([]byte("user-" + email)),
Email: email,
@ -41,7 +43,7 @@ func createTestUser(t *testing.T, store *inmemory.InMemoryStorage, email string)
return user
}
func createTestDomain(t *testing.T, store *inmemory.InMemoryStorage, user *happydns.User, domainName string) *happydns.Domain {
func createTestDomain(t *testing.T, store storage.Storage, user *happydns.User, domainName string) *happydns.Domain {
domain := &happydns.Domain{
Owner: user.Id,
DomainName: domainName,
@ -54,10 +56,11 @@ func createTestDomain(t *testing.T, store *inmemory.InMemoryStorage, user *happy
func Test_AppendDomainLog(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
logService := domainlog.NewService(mem)
db, _ := kv.NewKVDatabase(mem)
logService := domainlog.NewService(db)
user := createTestUser(t, mem, "test@example.com")
domain := createTestDomain(t, mem, user, "example.com")
user := createTestUser(t, db, "test@example.com")
domain := createTestDomain(t, db, user, "example.com")
log := happydns.NewDomainLog(user, happydns.LOG_INFO, "Test log entry")
@ -83,7 +86,7 @@ func Test_AppendDomainLog(t *testing.T) {
}
// Verify log is stored in database
logs, err := mem.ListDomainLogs(domain)
logs, err := db.ListDomainLogs(domain)
if err != nil {
t.Fatalf("expected stored logs, got error: %v", err)
}
@ -97,10 +100,11 @@ func Test_AppendDomainLog(t *testing.T) {
func Test_ListDomainLogs(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
logService := domainlog.NewService(mem)
db, _ := kv.NewKVDatabase(mem)
logService := domainlog.NewService(db)
user := createTestUser(t, mem, "test@example.com")
domain := createTestDomain(t, mem, user, "example.com")
user := createTestUser(t, db, "test@example.com")
domain := createTestDomain(t, db, user, "example.com")
// Create multiple logs with different timestamps
log1 := happydns.NewDomainLog(user, happydns.LOG_INFO, "First log")
@ -150,11 +154,12 @@ func Test_ListDomainLogs(t *testing.T) {
func Test_ListDomainLogs_MultipleDomains(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
logService := domainlog.NewService(mem)
db, _ := kv.NewKVDatabase(mem)
logService := domainlog.NewService(db)
user := createTestUser(t, mem, "test@example.com")
domain1 := createTestDomain(t, mem, user, "example.com")
domain2 := createTestDomain(t, mem, user, "test.com")
user := createTestUser(t, db, "test@example.com")
domain1 := createTestDomain(t, db, user, "example.com")
domain2 := createTestDomain(t, db, user, "test.com")
// Create logs for domain1
log1 := happydns.NewDomainLog(user, happydns.LOG_INFO, "Domain1 Log 1")
@ -197,10 +202,11 @@ func Test_ListDomainLogs_MultipleDomains(t *testing.T) {
func Test_UpdateDomainLog(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
logService := domainlog.NewService(mem)
db, _ := kv.NewKVDatabase(mem)
logService := domainlog.NewService(db)
user := createTestUser(t, mem, "test@example.com")
domain := createTestDomain(t, mem, user, "example.com")
user := createTestUser(t, db, "test@example.com")
domain := createTestDomain(t, db, user, "example.com")
// Create a log
log := happydns.NewDomainLog(user, happydns.LOG_INFO, "Original content")
@ -235,10 +241,11 @@ func Test_UpdateDomainLog(t *testing.T) {
func Test_DeleteDomainLog(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
logService := domainlog.NewService(mem)
db, _ := kv.NewKVDatabase(mem)
logService := domainlog.NewService(db)
user := createTestUser(t, mem, "test@example.com")
domain := createTestDomain(t, mem, user, "example.com")
user := createTestUser(t, db, "test@example.com")
domain := createTestDomain(t, db, user, "example.com")
// Create multiple logs
log1 := happydns.NewDomainLog(user, happydns.LOG_INFO, "Log 1")
@ -274,10 +281,11 @@ func Test_DeleteDomainLog(t *testing.T) {
func Test_AppendDomainLog_DifferentLogLevels(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
logService := domainlog.NewService(mem)
db, _ := kv.NewKVDatabase(mem)
logService := domainlog.NewService(db)
user := createTestUser(t, mem, "test@example.com")
domain := createTestDomain(t, mem, user, "example.com")
user := createTestUser(t, db, "test@example.com")
domain := createTestDomain(t, db, user, "example.com")
levels := []int8{
happydns.LOG_CRIT,
@ -311,10 +319,11 @@ func Test_AppendDomainLog_DifferentLogLevels(t *testing.T) {
func Test_ListDomainLogs_EmptyDomain(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
logService := domainlog.NewService(mem)
db, _ := kv.NewKVDatabase(mem)
logService := domainlog.NewService(db)
user := createTestUser(t, mem, "test@example.com")
domain := createTestDomain(t, mem, user, "example.com")
user := createTestUser(t, db, "test@example.com")
domain := createTestDomain(t, db, user, "example.com")
// List logs for a domain with no logs
logs, err := logService.ListDomainLogs(domain)

View file

@ -25,13 +25,15 @@ import (
"encoding/json"
"testing"
"git.happydns.org/happyDomain/internal/storage"
"git.happydns.org/happyDomain/internal/storage/inmemory"
kv "git.happydns.org/happyDomain/internal/storage/kvtpl"
"git.happydns.org/happyDomain/internal/usecase/provider"
"git.happydns.org/happyDomain/model"
"git.happydns.org/happyDomain/providers"
)
func createTestUser(t *testing.T, store *inmemory.InMemoryStorage, email string) *happydns.User {
func createTestUser(t *testing.T, store storage.Storage, email string) *happydns.User {
user := &happydns.User{
Id: happydns.Identifier([]byte("user-" + email)),
Email: email,
@ -74,11 +76,12 @@ func (v *mockValidator) Validate(p *happydns.Provider) error {
func Test_CreateProvider(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
providerService := provider.NewService(mem)
db, _ := kv.NewKVDatabase(mem)
providerService := provider.NewService(db)
// Replace validator with mock to avoid actual DNS validation
providerService.SetValidator(&mockValidator{})
user := createTestUser(t, mem, "test@example.com")
user := createTestUser(t, db, "test@example.com")
msg := createTestProviderMessage(t, "DDNSServer", "Test DDNS Provider")
p, err := providerService.CreateProvider(user, msg)
@ -97,7 +100,7 @@ func Test_CreateProvider(t *testing.T) {
}
// Verify provider is stored in database
stored, err := mem.GetProvider(p.Id)
stored, err := db.GetProvider(p.Id)
if err != nil {
t.Fatalf("expected stored provider, got error: %v", err)
}
@ -108,10 +111,11 @@ func Test_CreateProvider(t *testing.T) {
func Test_GetUserProvider(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
providerService := provider.NewService(mem)
db, _ := kv.NewKVDatabase(mem)
providerService := provider.NewService(db)
providerService.SetValidator(&mockValidator{})
user := createTestUser(t, mem, "test@example.com")
user := createTestUser(t, db, "test@example.com")
// Create a provider
msg := createTestProviderMessage(t, "DDNSServer", "Test Provider")
@ -136,11 +140,12 @@ func Test_GetUserProvider(t *testing.T) {
func Test_GetUserProvider_WrongUser(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
providerService := provider.NewService(mem)
db, _ := kv.NewKVDatabase(mem)
providerService := provider.NewService(db)
providerService.SetValidator(&mockValidator{})
user1 := createTestUser(t, mem, "user1@example.com")
user2 := createTestUser(t, mem, "user2@example.com")
user1 := createTestUser(t, db, "user1@example.com")
user2 := createTestUser(t, db, "user2@example.com")
// Create a provider for user1
msg := createTestProviderMessage(t, "DDNSServer", "User1 Provider")
@ -161,9 +166,10 @@ func Test_GetUserProvider_WrongUser(t *testing.T) {
func Test_GetUserProvider_NotFound(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
providerService := provider.NewService(mem)
db, _ := kv.NewKVDatabase(mem)
providerService := provider.NewService(db)
user := createTestUser(t, mem, "test@example.com")
user := createTestUser(t, db, "test@example.com")
nonexistentID := happydns.Identifier([]byte("nonexistent-id"))
_, err := providerService.GetUserProvider(user, nonexistentID)
@ -177,10 +183,11 @@ func Test_GetUserProvider_NotFound(t *testing.T) {
func Test_GetUserProviderMeta(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
providerService := provider.NewService(mem)
db, _ := kv.NewKVDatabase(mem)
providerService := provider.NewService(db)
providerService.SetValidator(&mockValidator{})
user := createTestUser(t, mem, "test@example.com")
user := createTestUser(t, db, "test@example.com")
// Create a provider
msg := createTestProviderMessage(t, "DDNSServer", "Test Provider Meta")
@ -205,10 +212,11 @@ func Test_GetUserProviderMeta(t *testing.T) {
func Test_ListUserProviders(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
providerService := provider.NewService(mem)
db, _ := kv.NewKVDatabase(mem)
providerService := provider.NewService(db)
providerService.SetValidator(&mockValidator{})
user := createTestUser(t, mem, "test@example.com")
user := createTestUser(t, db, "test@example.com")
// Create multiple providers
_, err := providerService.CreateProvider(user, createTestProviderMessage(t, "DDNSServer", "Provider 1"))
@ -237,11 +245,12 @@ func Test_ListUserProviders(t *testing.T) {
func Test_ListUserProviders_MultipleUsers(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
providerService := provider.NewService(mem)
db, _ := kv.NewKVDatabase(mem)
providerService := provider.NewService(db)
providerService.SetValidator(&mockValidator{})
user1 := createTestUser(t, mem, "user1@example.com")
user2 := createTestUser(t, mem, "user2@example.com")
user1 := createTestUser(t, db, "user1@example.com")
user2 := createTestUser(t, db, "user2@example.com")
// Create providers for user1
_, err := providerService.CreateProvider(user1, createTestProviderMessage(t, "DDNSServer", "User1 Provider 1"))
@ -280,10 +289,11 @@ func Test_ListUserProviders_MultipleUsers(t *testing.T) {
func Test_UpdateProvider(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
providerService := provider.NewService(mem)
db, _ := kv.NewKVDatabase(mem)
providerService := provider.NewService(db)
providerService.SetValidator(&mockValidator{})
user := createTestUser(t, mem, "test@example.com")
user := createTestUser(t, db, "test@example.com")
// Create a provider
msg := createTestProviderMessage(t, "DDNSServer", "Original comment")
@ -312,10 +322,11 @@ func Test_UpdateProvider(t *testing.T) {
func Test_UpdateProvider_PreventIdChange(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
providerService := provider.NewService(mem)
db, _ := kv.NewKVDatabase(mem)
providerService := provider.NewService(db)
providerService.SetValidator(&mockValidator{})
user := createTestUser(t, mem, "test@example.com")
user := createTestUser(t, db, "test@example.com")
// Create a provider
msg := createTestProviderMessage(t, "DDNSServer", "Test Provider")
@ -339,11 +350,12 @@ func Test_UpdateProvider_PreventIdChange(t *testing.T) {
func Test_UpdateProvider_WrongUser(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
providerService := provider.NewService(mem)
db, _ := kv.NewKVDatabase(mem)
providerService := provider.NewService(db)
providerService.SetValidator(&mockValidator{})
user1 := createTestUser(t, mem, "user1@example.com")
user2 := createTestUser(t, mem, "user2@example.com")
user1 := createTestUser(t, db, "user1@example.com")
user2 := createTestUser(t, db, "user2@example.com")
// Create a provider for user1
msg := createTestProviderMessage(t, "DDNSServer", "User1 Provider")
@ -363,10 +375,11 @@ func Test_UpdateProvider_WrongUser(t *testing.T) {
func Test_DeleteProvider(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
providerService := provider.NewService(mem)
db, _ := kv.NewKVDatabase(mem)
providerService := provider.NewService(db)
providerService.SetValidator(&mockValidator{})
user := createTestUser(t, mem, "test@example.com")
user := createTestUser(t, db, "test@example.com")
// Create a provider
msg := createTestProviderMessage(t, "DDNSServer", "Test Provider")
@ -426,12 +439,13 @@ func Test_ParseProvider_InvalidType(t *testing.T) {
func Test_RestrictedService_CreateProvider_Disabled(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
db, _ := kv.NewKVDatabase(mem)
config := &happydns.Options{
DisableProviders: true,
}
providerService := provider.NewRestrictedService(config, mem)
providerService := provider.NewRestrictedService(config, db)
user := createTestUser(t, mem, "test@example.com")
user := createTestUser(t, db, "test@example.com")
msg := createTestProviderMessage(t, "DDNSServer", "Test Provider")
_, err := providerService.CreateProvider(user, msg)
@ -445,11 +459,12 @@ func Test_RestrictedService_CreateProvider_Disabled(t *testing.T) {
func Test_RestrictedService_UpdateProvider_Disabled(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
db, _ := kv.NewKVDatabase(mem)
// First create a provider without restrictions
unrestricted := provider.NewService(mem)
unrestricted := provider.NewService(db)
unrestricted.SetValidator(&mockValidator{})
user := createTestUser(t, mem, "test@example.com")
user := createTestUser(t, db, "test@example.com")
msg := createTestProviderMessage(t, "DDNSServer", "Test Provider")
createdProvider, err := unrestricted.CreateProvider(user, msg)
if err != nil {
@ -460,7 +475,7 @@ func Test_RestrictedService_UpdateProvider_Disabled(t *testing.T) {
config := &happydns.Options{
DisableProviders: true,
}
restrictedService := provider.NewRestrictedService(config, mem)
restrictedService := provider.NewRestrictedService(config, db)
err = restrictedService.UpdateProvider(createdProvider.Id, user, func(p *happydns.Provider) {
p.Comment = "Updated"
@ -475,11 +490,12 @@ func Test_RestrictedService_UpdateProvider_Disabled(t *testing.T) {
func Test_RestrictedService_DeleteProvider_Disabled(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
db, _ := kv.NewKVDatabase(mem)
// First create a provider without restrictions
unrestricted := provider.NewService(mem)
unrestricted := provider.NewService(db)
unrestricted.SetValidator(&mockValidator{})
user := createTestUser(t, mem, "test@example.com")
user := createTestUser(t, db, "test@example.com")
msg := createTestProviderMessage(t, "DDNSServer", "Test Provider")
createdProvider, err := unrestricted.CreateProvider(user, msg)
if err != nil {
@ -490,7 +506,7 @@ func Test_RestrictedService_DeleteProvider_Disabled(t *testing.T) {
config := &happydns.Options{
DisableProviders: true,
}
restrictedService := provider.NewRestrictedService(config, mem)
restrictedService := provider.NewRestrictedService(config, db)
err = restrictedService.DeleteProvider(user, createdProvider.Id)
if err == nil {

View file

@ -25,12 +25,14 @@ import (
"testing"
"time"
"git.happydns.org/happyDomain/internal/storage"
"git.happydns.org/happyDomain/internal/storage/inmemory"
kv "git.happydns.org/happyDomain/internal/storage/kvtpl"
"git.happydns.org/happyDomain/internal/usecase/session"
"git.happydns.org/happyDomain/model"
)
func createTestUser(t *testing.T, store *inmemory.InMemoryStorage, email string) *happydns.User {
func createTestUser(t *testing.T, store storage.Storage, email string) *happydns.User {
user := &happydns.User{
Id: happydns.Identifier([]byte("user-" + email)),
Email: email,
@ -43,9 +45,10 @@ func createTestUser(t *testing.T, store *inmemory.InMemoryStorage, email string)
func Test_CreateUserSession(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
sessionService := session.NewService(mem)
db, _ := kv.NewKVDatabase(mem)
sessionService := session.NewService(db)
user := createTestUser(t, mem, "test@example.com")
user := createTestUser(t, db, "test@example.com")
sess, err := sessionService.CreateUserSession(user, "Test session")
if err != nil {
@ -69,7 +72,7 @@ func Test_CreateUserSession(t *testing.T) {
}
// Verify session is stored in database
stored, err := mem.GetSession(sess.Id)
stored, err := db.GetSession(sess.Id)
if err != nil {
t.Fatalf("expected stored session, got error: %v", err)
}
@ -80,9 +83,10 @@ func Test_CreateUserSession(t *testing.T) {
func Test_GetUserSession(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
sessionService := session.NewService(mem)
db, _ := kv.NewKVDatabase(mem)
sessionService := session.NewService(db)
user := createTestUser(t, mem, "test@example.com")
user := createTestUser(t, db, "test@example.com")
// Create a session
createdSession, err := sessionService.CreateUserSession(user, "Test session")
@ -106,10 +110,11 @@ func Test_GetUserSession(t *testing.T) {
func Test_GetUserSession_WrongUser(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
sessionService := session.NewService(mem)
db, _ := kv.NewKVDatabase(mem)
sessionService := session.NewService(db)
user1 := createTestUser(t, mem, "user1@example.com")
user2 := createTestUser(t, mem, "user2@example.com")
user1 := createTestUser(t, db, "user1@example.com")
user2 := createTestUser(t, db, "user2@example.com")
// Create a session for user1
createdSession, err := sessionService.CreateUserSession(user1, "User1 session")
@ -129,9 +134,10 @@ func Test_GetUserSession_WrongUser(t *testing.T) {
func Test_GetUserSession_NotFound(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
sessionService := session.NewService(mem)
db, _ := kv.NewKVDatabase(mem)
sessionService := session.NewService(db)
user := createTestUser(t, mem, "test@example.com")
user := createTestUser(t, db, "test@example.com")
_, err := sessionService.GetUserSession(user, "nonexistent-session-id")
if err == nil {
@ -144,9 +150,10 @@ func Test_GetUserSession_NotFound(t *testing.T) {
func Test_ListUserSessions(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
sessionService := session.NewService(mem)
db, _ := kv.NewKVDatabase(mem)
sessionService := session.NewService(db)
user := createTestUser(t, mem, "test@example.com")
user := createTestUser(t, db, "test@example.com")
// Create multiple sessions
_, err := sessionService.CreateUserSession(user, "Session 1")
@ -175,10 +182,11 @@ func Test_ListUserSessions(t *testing.T) {
func Test_ListUserSessions_MultipleUsers(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
sessionService := session.NewService(mem)
db, _ := kv.NewKVDatabase(mem)
sessionService := session.NewService(db)
user1 := createTestUser(t, mem, "user1@example.com")
user2 := createTestUser(t, mem, "user2@example.com")
user1 := createTestUser(t, db, "user1@example.com")
user2 := createTestUser(t, db, "user2@example.com")
// Create sessions for user1
_, err := sessionService.CreateUserSession(user1, "User1 Session 1")
@ -217,9 +225,10 @@ func Test_ListUserSessions_MultipleUsers(t *testing.T) {
func Test_UpdateUserSession(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
sessionService := session.NewService(mem)
db, _ := kv.NewKVDatabase(mem)
sessionService := session.NewService(db)
user := createTestUser(t, mem, "test@example.com")
user := createTestUser(t, db, "test@example.com")
// Create a session
createdSession, err := sessionService.CreateUserSession(user, "Original description")
@ -250,9 +259,10 @@ func Test_UpdateUserSession(t *testing.T) {
func Test_UpdateUserSession_PreventIdChange(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
sessionService := session.NewService(mem)
db, _ := kv.NewKVDatabase(mem)
sessionService := session.NewService(db)
user := createTestUser(t, mem, "test@example.com")
user := createTestUser(t, db, "test@example.com")
// Create a session
createdSession, err := sessionService.CreateUserSession(user, "Test session")
@ -274,10 +284,11 @@ func Test_UpdateUserSession_PreventIdChange(t *testing.T) {
func Test_UpdateUserSession_WrongUser(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
sessionService := session.NewService(mem)
db, _ := kv.NewKVDatabase(mem)
sessionService := session.NewService(db)
user1 := createTestUser(t, mem, "user1@example.com")
user2 := createTestUser(t, mem, "user2@example.com")
user1 := createTestUser(t, db, "user1@example.com")
user2 := createTestUser(t, db, "user2@example.com")
// Create a session for user1
createdSession, err := sessionService.CreateUserSession(user1, "User1 session")
@ -296,9 +307,10 @@ func Test_UpdateUserSession_WrongUser(t *testing.T) {
func Test_DeleteUserSession(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
sessionService := session.NewService(mem)
db, _ := kv.NewKVDatabase(mem)
sessionService := session.NewService(db)
user := createTestUser(t, mem, "test@example.com")
user := createTestUser(t, db, "test@example.com")
// Create a session
createdSession, err := sessionService.CreateUserSession(user, "Test session")
@ -324,10 +336,11 @@ func Test_DeleteUserSession(t *testing.T) {
func Test_DeleteUserSession_WrongUser(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
sessionService := session.NewService(mem)
db, _ := kv.NewKVDatabase(mem)
sessionService := session.NewService(db)
user1 := createTestUser(t, mem, "user1@example.com")
user2 := createTestUser(t, mem, "user2@example.com")
user1 := createTestUser(t, db, "user1@example.com")
user2 := createTestUser(t, db, "user2@example.com")
// Create a session for user1
createdSession, err := sessionService.CreateUserSession(user1, "User1 session")
@ -350,9 +363,10 @@ func Test_DeleteUserSession_WrongUser(t *testing.T) {
func Test_CloseUserSessions(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
sessionService := session.NewService(mem)
db, _ := kv.NewKVDatabase(mem)
sessionService := session.NewService(db)
user := createTestUser(t, mem, "test@example.com")
user := createTestUser(t, db, "test@example.com")
// Create multiple sessions
_, err := sessionService.CreateUserSession(user, "Session 1")
@ -386,10 +400,11 @@ func Test_CloseUserSessions(t *testing.T) {
func Test_CloseUserSessions_MultipleUsers(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
sessionService := session.NewService(mem)
db, _ := kv.NewKVDatabase(mem)
sessionService := session.NewService(db)
user1 := createTestUser(t, mem, "user1@example.com")
user2 := createTestUser(t, mem, "user2@example.com")
user1 := createTestUser(t, db, "user1@example.com")
user2 := createTestUser(t, db, "user2@example.com")
// Create sessions for both users
_, err := sessionService.CreateUserSession(user1, "User1 Session")
@ -439,14 +454,15 @@ func (u testUserInfo) JoinNewsletter() bool { return false }
func Test_CloseAll_UserInfoInterface(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
sessionService := session.NewService(mem)
db, _ := kv.NewKVDatabase(mem)
sessionService := session.NewService(db)
userID := happydns.Identifier([]byte("user-123"))
user := &happydns.User{
Id: userID,
Email: "test@example.com",
}
if err := mem.CreateOrUpdateUser(user); err != nil {
if err := db.CreateOrUpdateUser(user); err != nil {
t.Fatalf("failed to create test user: %v", err)
}
@ -479,14 +495,15 @@ func Test_CloseAll_UserInfoInterface(t *testing.T) {
func Test_ByID(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
sessionService := session.NewService(mem)
db, _ := kv.NewKVDatabase(mem)
sessionService := session.NewService(db)
userID := happydns.Identifier([]byte("user-123"))
user := &happydns.User{
Id: userID,
Email: "test@example.com",
}
if err := mem.CreateOrUpdateUser(user); err != nil {
if err := db.CreateOrUpdateUser(user); err != nil {
t.Fatalf("failed to create test user: %v", err)
}
@ -534,9 +551,10 @@ func Test_NewSessionID(t *testing.T) {
func Test_SessionExpiration(t *testing.T) {
mem, _ := inmemory.NewInMemoryStorage()
sessionService := session.NewService(mem)
db, _ := kv.NewKVDatabase(mem)
sessionService := session.NewService(db)
user := createTestUser(t, mem, "test@example.com")
user := createTestUser(t, db, "test@example.com")
// Create a session
sess, err := sessionService.CreateUserSession(user, "Test session")

View file

@ -25,7 +25,9 @@ import (
"bytes"
"testing"
"git.happydns.org/happyDomain/internal/storage"
"git.happydns.org/happyDomain/internal/storage/inmemory"
kv "git.happydns.org/happyDomain/internal/storage/kvtpl"
authuserUC "git.happydns.org/happyDomain/internal/usecase/authuser"
sessionUC "git.happydns.org/happyDomain/internal/usecase/session"
"git.happydns.org/happyDomain/internal/usecase/user"
@ -70,8 +72,9 @@ func (u testUserInfo) GetUserId() happydns.Identifier { return u.id }
func (u testUserInfo) GetEmail() string { return u.email }
func (u testUserInfo) JoinNewsletter() bool { return u.joinNewsletter }
func createTestService(t *testing.T) (*user.Service, *inmemory.InMemoryStorage, *mockNewsletterSubscriptor, *mockSessionCloser) {
mem, err := inmemory.NewInMemoryStorage()
func createTestService(t *testing.T) (*user.Service, storage.Storage, *mockNewsletterSubscriptor, *mockSessionCloser) {
mem, _ := inmemory.NewInMemoryStorage()
db, err := kv.NewKVDatabase(mem)
if err != nil {
t.Fatalf("failed to create in-memory storage: %v", err)
}
@ -79,15 +82,15 @@ func createTestService(t *testing.T) (*user.Service, *inmemory.InMemoryStorage,
cfg := &happydns.Options{
DisableRegistration: false,
}
sessionService := sessionUC.NewService(mem)
authUserService := authuserUC.NewAuthUserUsecases(cfg, nil, mem, sessionService)
sessionService := sessionUC.NewService(db)
authUserService := authuserUC.NewAuthUserUsecases(cfg, nil, db, sessionService)
newsletter := &mockNewsletterSubscriptor{}
sessionCloser := &mockSessionCloser{}
service := user.NewUserUsecases(mem, newsletter, authUserService, sessionCloser)
service := user.NewUserUsecases(db, newsletter, authUserService, sessionCloser)
return service, mem, newsletter, sessionCloser
return service, db, newsletter, sessionCloser
}
func Test_CreateUser(t *testing.T) {

View file

@ -35,6 +35,7 @@ var (
ErrUserNotFound = errors.New("user not found")
ErrUserAlreadyExist = errors.New("user already exists")
ErrZoneNotFound = errors.New("zone not found")
ErrNotFound = errors.New("not found")
)
const TryAgainErr = "Sorry, we are currently unable to sent email validation link. Please try again later."

View file

@ -39,9 +39,13 @@ type Iterator[T any] interface {
// Must be called only after a successful call to Next().
DropItem() error
// Key returns the storage key of the current item.
// Should be called only after a successful call to Next().
Key() string
// Raw returns the raw (non-decoded) value at the current iterator position.
// Should only be called after a successful call to Next().
Raw() []byte
Raw() interface{}
// Err returns the first error encountered during iteration, if any.
Err() error