refac: stale test entries, failing web tests, Postgre backup bug, Hardcoded MySQL table count, Stale Postgre connection in test, own-scope visibility filter, GetAll ignoring its error, JSON tags in use-case types, 31 copies of DESKTOP_QUERY, ProfileView endpoint paths

This commit is contained in:
Florian Sylvain committed 2026-10-28 17:54:10 +01:00
1 parent efecdee200
commit 5d6f378fce
87 files changed
+696 -373

No files matched your search

@@ -30,14 +30,15 @@ func DefaultSort() Sort {
}
type Filters struct {
IsOnline *bool
SiteID *uint32
Page int
Size int
Q string
Status string
Sort Sort
Unpaged bool
IsOnline *bool
SiteID *uint32
VisibleTo *uint32
Page int
Size int
Q string
Status string
Sort Sort
Unpaged bool
}
type Nullable[T any] struct {
@@ -89,9 +90,10 @@ type Repository interface {
FindByFilters(filters Filters) ([]Article, error)
FindPage(filters Filters) (PageResult, error)
CountByVisibility(onlineOnly bool) (Counts, error)
CountVisibleTo(userID uint32) (Counts, error)
Get(id uint32) (Article, error)
GetByName(name string) (Article, error)
GetAll() []Article
GetAll() ([]Article, error)
Create(post Article) (Article, error)
UpdateBody(id uint32, body string) (Article, error)
Update(id uint32, update Update) (Article, error)
@@ -48,6 +48,7 @@ func DefaultSort() Sort {
type Filters struct {
ContentTypeID uint32
CreatedBy *uint32
Page int
Size int
Sort Sort
@@ -7,7 +7,7 @@ import (
type Repository interface {
Create(invite Invite) (Invite, error)
GetAll() []Invite
GetAll() ([]Invite, error)
Delete(id uint32) error
Renew(id uint32, hashedCode string, expiresAt time.Time, createdAt time.Time) (Invite, error)
AcceptWithUser(inviteID uint32, acceptedAt time.Time, newUser user.User) (user.User, error)
@@ -6,7 +6,7 @@ type Repository interface {
Get(id uint32) (User, error)
GetByUsername(username string) (User, error)
GetByEmail(email string) (User, error)
GetAll() []User
GetAll() ([]User, error)
FindByIDs(ids []uint32) ([]User, error)
Create(user User) (User, error)
Delete(id uint32) error
@@ -48,6 +48,8 @@ var postgresNumericTypes = map[string]bool{
"INT2": true, "INT4": true, "INT8": true, "FLOAT4": true, "FLOAT8": true, "NUMERIC": true, "OID": true,
}
var postgresIntegerTypes = map[string]bool{"INT2": true, "INT4": true, "INT8": true}
var postgresSerialTypes = map[string]string{
"smallint": "smallserial",
"integer": "serial",
@@ -175,7 +177,7 @@ func postgresLiteral(databaseType string, value []byte) string {
func postgresSequenceReset(table string, columns []DumpColumn) []string {
for _, column := range columns {
if column.Name != postgresIdentityColumn {
if column.Name != postgresIdentityColumn || !postgresIntegerTypes[strings.ToUpper(column.Type)] {
continue
}
quotedTable, quotedColumn := quotePostgresIdentifier(table), quotePostgresIdentifier(column.Name)
@@ -46,6 +46,9 @@ func (a *ArticleRepository) scoped(filters domain.Filters) *gorm.DB {
if filters.SiteID != nil {
query = query.Where("site_id = ?", *filters.SiteID)
}
if filters.VisibleTo != nil {
query = query.Where("(is_online = ? OR created_by = ?)", true, *filters.VisibleTo)
}
switch filters.Status {
case domain.StatusOnline:
query = query.Where("is_online = ?", true)
@@ -168,6 +171,17 @@ func (a *ArticleRepository) CountByVisibility(onlineOnly bool) (domain.Counts, e
return domain.Counts{All: int(online + draft), Online: int(online), Draft: int(draft)}, nil
}
func (a *ArticleRepository) CountVisibleTo(userID uint32) (domain.Counts, error) {
var online, draft int64
if err := a.db.Model(&entity.Article{}).Where("is_online = ?", true).Count(&online).Error; err != nil {
return domain.Counts{}, err
}
if err := a.db.Model(&entity.Article{}).Where("is_online = ? AND created_by = ?", false, userID).Count(&draft).Error; err != nil {
return domain.Counts{}, err
}
return domain.Counts{All: int(online + draft), Online: int(online), Draft: int(draft)}, nil
}
func toDomainArticles(entityArticles []entity.Article) []domain.Article {
domainArticles := make([]domain.Article, 0, len(entityArticles))
for _, entityArticle := range entityArticles {
@@ -356,11 +370,11 @@ func (a *ArticleRepository) SlugTaken(siteID uint32, slug string, exceptID uint3
return articleSlugInUse(a.db, siteID, slug, exceptID)
}
func (a *ArticleRepository) GetAll() []domain.Article {
func (a *ArticleRepository) GetAll() ([]domain.Article, error) {
var articles []entity.Article
err := a.db.Model(&entity.Article{}).Find(&articles).Error
if err != nil {
return []domain.Article{}
return nil, err
}
var domainArticles = make([]domain.Article, 0)
@@ -368,7 +382,7 @@ func (a *ArticleRepository) GetAll() []domain.Article {
domainArticles = append(domainArticles, mappers.ArticleToDomain(article))
}
return domainArticles
return domainArticles, nil
}
func (a *ArticleRepository) UpdateBody(id uint32, body string) (domain.Article, error) {
@@ -71,7 +71,11 @@ func (e *EntryRepository) ListPage(filters domain.Filters) (domain.PageResult, e
filters.Page = max(filters.Page, 1)
filters.Size = max(filters.Size, 0)
scoped := func() *gorm.DB {
return e.db.Model(&entity.Entry{}).Where("content_type_id = ?", filters.ContentTypeID)
query := e.db.Model(&entity.Entry{}).Where("content_type_id = ?", filters.ContentTypeID)
if filters.CreatedBy != nil {
query = query.Where("created_by = ?", *filters.CreatedBy)
}
return query
}
var total int64
if err := scoped().Count(&total).Error; err != nil {
@@ -28,16 +28,18 @@ func (i *InviteRepository) Create(invite domainInvite.Invite) (domainInvite.Invi
return mappers.InviteToDomain(model), nil
}
func (i *InviteRepository) GetAll() []domainInvite.Invite {
func (i *InviteRepository) GetAll() ([]domainInvite.Invite, error) {
var invites []entity.Invite
i.db.Model(&entity.Invite{}).Find(&invites)
if err := i.db.Model(&entity.Invite{}).Find(&invites).Error; err != nil {
return nil, err
}
var domainInvites []domainInvite.Invite
for _, localInvite := range invites {
domainInvites = append(domainInvites, mappers.InviteToDomain(localInvite))
}
return domainInvites
return domainInvites, nil
}
func (i *InviteRepository) Delete(id uint32) error {
@@ -52,16 +52,18 @@ func (u *UserRepository) Delete(id uint32) error {
return deleteOrNotFound(result, domainUser.ErrUserNotFound)
}
func (u *UserRepository) GetAll() []domainUser.User {
func (u *UserRepository) GetAll() ([]domainUser.User, error) {
var users []entity.User
u.db.Model(&entity.User{}).Find(&users)
if err := u.db.Model(&entity.User{}).Find(&users).Error; err != nil {
return nil, err
}
var domainUsers []domainUser.User
for _, localUser := range users {
domainUsers = append(domainUsers, mappers.UserToDomain(localUser))
}
return domainUsers
return domainUsers, nil
}
func (u *UserRepository) FindByIDs(ids []uint32) ([]domainUser.User, error) {
@@ -72,7 +72,11 @@ func (a *AcceptInviteUseCase) recordRefusal(ctx context.Context, event audit.Eve
}
func (a *AcceptInviteUseCase) accept(command AcceptInviteCommand, pending invite.Invite, assigned role.Role, now time.Time) (user.User, error) {
if a.emailHasAccount(pending.Email) {
hasAccount, err := a.emailHasAccount(pending.Email)
if err != nil {
return user.User{}, err
}
if hasAccount {
if _, err := findUsableInvite(a.inviteRepository, a.hasher, command.Code, now); err != nil {
return user.User{}, err
}
@@ -122,13 +126,17 @@ func (a *AcceptInviteUseCase) inviterOf(pending invite.Invite) (uint32, error) {
return assigner.ID, nil
}
func (a *AcceptInviteUseCase) emailHasAccount(email string) bool {
for _, existingUser := range a.userRepository.GetAll() {
func (a *AcceptInviteUseCase) emailHasAccount(email string) (bool, error) {
users, err := a.userRepository.GetAll()
if err != nil {
return false, err
}
for _, existingUser := range users {
if strings.EqualFold(existingUser.Email, email) {
return true
return true, nil
}
}
return false
return false, nil
}
func (a *AcceptInviteUseCase) ensureUsernameFree(username string) error {
@@ -98,7 +98,11 @@ func (g *CreateInviteUseCase) create(command CreateInviteCommand) (invite.Invite
return invite.Invite{}, role.Role{}, err
}
if g.emailHasAccount(email) {
hasAccount, err := g.emailHasAccount(email)
if err != nil {
return invite.Invite{}, role.Role{}, err
}
if hasAccount {
return invite.Invite{}, role.Role{}, invite.ErrEmailHasAccount
}
@@ -133,17 +137,25 @@ func (g *CreateInviteUseCase) create(command CreateInviteCommand) (invite.Invite
return createdInvite, assigned, nil
}
func (g *CreateInviteUseCase) emailHasAccount(email string) bool {
for _, existingUser := range g.userRepository.GetAll() {
func (g *CreateInviteUseCase) emailHasAccount(email string) (bool, error) {
users, err := g.userRepository.GetAll()
if err != nil {
return false, err
}
for _, existingUser := range users {
if strings.EqualFold(existingUser.Email, email) {
return true
return true, nil
}
}
return false
return false, nil
}
func (g *CreateInviteUseCase) discardOpenInvite(email string, now time.Time) error {
for _, existing := range g.inviteRepository.GetAll() {
invites, err := g.inviteRepository.GetAll()
if err != nil {
return err
}
for _, existing := range invites {
if existing.Email != email {
continue
}
+16 -7
View File
@@ -53,7 +53,11 @@ func (g *CreateUserUseCase) CreateUser(ctx context.Context, createUser CreateUse
}
func (g *CreateUserUseCase) create(createUser CreateUserCommand) (user.User, error) {
if g.registrationClosed() {
closed, err := g.registrationClosed()
if err != nil {
return user.User{}, err
}
if closed {
return user.User{}, user.ErrRegistrationClosed
}
@@ -108,15 +112,20 @@ func (g *CreateUserUseCase) create(createUser CreateUserCommand) (user.User, err
return createdUser, nil
}
func (g *CreateUserUseCase) IsRegistrationOpen() bool {
return !g.registrationClosed()
func (g *CreateUserUseCase) IsRegistrationOpen() (bool, error) {
closed, err := g.registrationClosed()
return !closed, err
}
func (g *CreateUserUseCase) registrationClosed() bool {
for _, existingUser := range g.userRepository.GetAll() {
func (g *CreateUserUseCase) registrationClosed() (bool, error) {
users, err := g.userRepository.GetAll()
if err != nil {
return false, err
}
for _, existingUser := range users {
if existingUser.IsVerified {
return true
return true, nil
}
}
return false
return false, nil
}
@@ -34,7 +34,11 @@ func (e *EnforceRetentionUseCase) Tick(now time.Time) (retention.Result, error)
func (e *EnforceRetentionUseCase) deleteStaleUnverifiedUsers(now time.Time) (int, error) {
removed := 0
for _, candidate := range e.users.GetAll() {
users, err := e.users.GetAll()
if err != nil {
return 0, err
}
for _, candidate := range users {
if !candidate.IsStaleUnverifiedAt(now) {
continue
}
@@ -48,7 +52,11 @@ func (e *EnforceRetentionUseCase) deleteStaleUnverifiedUsers(now time.Time) (int
func (e *EnforceRetentionUseCase) deleteStaleInvites(now time.Time) (int, error) {
removed := 0
for _, candidate := range e.invites.GetAll() {
invites, err := e.invites.GetAll()
if err != nil {
return 0, err
}
for _, candidate := range invites {
if !candidate.IsStaleAt(now) {
continue
}
@@ -24,7 +24,11 @@ func findUsableInvite(inviteRepository invite.Repository, hasher hash.Hasher, co
return invite.Invite{}, invite.ErrInviteInvalid
}
for _, candidate := range inviteRepository.GetAll() {
invites, err := inviteRepository.GetAll()
if err != nil {
return invite.Invite{}, err
}
for _, candidate := range invites {
if candidate.AcceptedAt != nil {
continue
}
@@ -42,20 +42,42 @@ type ArticlePage struct {
func (u *ListArticlesUseCase) ListArticlesPage(command ListArticlesCommand) (ArticlePage, error) {
switch command.Caller.Grants.ScopeFor(role.KeyContentRead, builtintype.KeyArticle) {
case role.ScopeAll:
return u.listVisibleByRepository(command, false)
return u.listVisibleByRepository(command, articleVisibility{})
case role.ScopeOwn:
return u.listOwnedAndOnline(command)
return u.listVisibleByRepository(command, articleVisibility{userID: &command.Caller.UserID})
default:
return u.listVisibleByRepository(command, true)
return u.listVisibleByRepository(command, articleVisibility{onlineOnly: true})
}
}
func (u *ListArticlesUseCase) listVisibleByRepository(command ListArticlesCommand, onlineOnly bool) (ArticlePage, error) {
counts, err := u.articleRepository.CountByVisibility(onlineOnly)
type articleVisibility struct {
onlineOnly bool
userID *uint32
}
func (v articleVisibility) apply(filters article.Filters) article.Filters {
if v.onlineOnly {
online := true
filters.IsOnline = &online
}
filters.VisibleTo = v.userID
return filters
}
func (u *ListArticlesUseCase) countVisible(visibility articleVisibility) (article.Counts, error) {
if visibility.userID != nil {
return u.articleRepository.CountVisibleTo(*visibility.userID)
}
return u.articleRepository.CountByVisibility(visibility.onlineOnly)
}
func (u *ListArticlesUseCase) listVisibleByRepository(command ListArticlesCommand, visibility articleVisibility) (ArticlePage, error) {
onlineOnly := visibility.onlineOnly
counts, err := u.countVisible(visibility)
if err != nil {
return ArticlePage{}, err
}
summaries, err := u.summarizeVisible(onlineOnly)
summaries, err := u.summarizeVisible(visibility)
if err != nil {
return ArticlePage{}, err
}
@@ -64,7 +86,7 @@ func (u *ListArticlesUseCase) listVisibleByRepository(command ListArticlesComman
if onlineOnly && command.Status == article.StatusDraft {
return page, nil
}
filters := pageFilters(command, onlineOnly)
filters := visibility.apply(pageFilters(command))
if command.SiteKey != nil {
siteID, known, err := u.siteIDOf(*command.SiteKey)
if err != nil {
@@ -98,71 +120,6 @@ func emptyArticlePage(command ListArticlesCommand, counts article.Counts) Articl
return ArticlePage{Items: []article.Article{}, Page: command.Page, Size: command.Size, Counts: counts, SEO: map[uint32]article.SEOSummary{}}
}
func (u *ListArticlesUseCase) listOwnedAndOnline(command ListArticlesCommand) (ArticlePage, error) {
everything, err := u.articleRepository.FindPage(article.Filters{Unpaged: true})
if err != nil {
return ArticlePage{}, err
}
visible := visibleTo(command.Caller, everything.Items)
summaries, err := u.summarize(visible)
if err != nil {
return ArticlePage{}, err
}
counts := tally(visible)
counts.Suggestions = countSuggestions(summaries)
page := emptyArticlePage(command, counts)
filters := pageFilters(command, false)
filters.Unpaged = true
if command.SiteKey != nil {
siteID, known, err := u.siteIDOf(*command.SiteKey)
if err != nil {
return ArticlePage{}, err
}
if !known {
return page, nil
}
filters.SiteID = &siteID
}
found, err := u.articleRepository.FindPage(filters)
if err != nil {
return ArticlePage{}, err
}
matching := visibleTo(command.Caller, found.Items)
if command.SEO != "" {
matching = withSEOStatus(matching, summaries, command.SEO)
}
rendered, err := renderArticles(u.bodyRenderer, pageSlice(matching, command.Page, command.Size), command.Format)
if err != nil {
return ArticlePage{}, err
}
page.Items = rendered
page.Total = len(matching)
page.SEO = summaries
return page, nil
}
func visibleTo(caller ContentCaller, items []article.Article) []article.Article {
visible := make([]article.Article, 0, len(items))
for _, item := range items {
if caller.seesArticle(item) {
visible = append(visible, item)
}
}
return visible
}
func tally(visible []article.Article) article.Counts {
counts := article.Counts{All: len(visible)}
for _, item := range visible {
if item.IsOnline {
counts.Online++
} else {
counts.Draft++
}
}
return counts
}
func withSEOStatus(items []article.Article, summaries map[uint32]article.SEOSummary, status string) []article.Article {
matching := make([]article.Article, 0, len(items))
for _, item := range items {
@@ -187,13 +144,8 @@ func pageSlice[T any](items []T, page int, size int) []T {
return items[start:min(start+size, len(items))]
}
func (u *ListArticlesUseCase) summarizeVisible(onlineOnly bool) (map[uint32]article.SEOSummary, error) {
filters := article.Filters{Unpaged: true}
if onlineOnly {
online := true
filters.IsOnline = &online
}
found, err := u.articleRepository.FindPage(filters)
func (u *ListArticlesUseCase) summarizeVisible(visibility articleVisibility) (map[uint32]article.SEOSummary, error) {
found, err := u.articleRepository.FindPage(visibility.apply(article.Filters{Unpaged: true}))
if err != nil {
return nil, err
}
@@ -245,11 +197,6 @@ func (u *ListArticlesUseCase) siteIDOf(key string) (uint32, bool, error) {
return found.ID, true, nil
}
func pageFilters(command ListArticlesCommand, onlineOnly bool) article.Filters {
filters := article.Filters{Page: command.Page, Size: command.Size, Q: command.Q, Status: command.Status, Sort: command.Sort}
if onlineOnly {
online := true
filters.IsOnline = &online
}
return filters
func pageFilters(command ListArticlesCommand) article.Filters {
return article.Filters{Page: command.Page, Size: command.Size, Q: command.Q, Status: command.Status, Sort: command.Sort}
}
@@ -70,7 +70,14 @@ func (u *ListEntriesUseCase) page(owner contenttype.ContentType, command ListEnt
if query == "" && readScope == role.ScopeAll {
return u.entries.ListPage(entry.Filters{ContentTypeID: owner.ID, Page: command.Page, Size: command.Size, Sort: command.Sort})
}
all, err := u.entries.ListPage(entry.Filters{ContentTypeID: owner.ID, Unpaged: true, Sort: command.Sort})
if query == "" && readScope == role.ScopeOwn {
return u.entries.ListPage(entry.Filters{ContentTypeID: owner.ID, CreatedBy: &command.Caller.UserID, Page: command.Page, Size: command.Size, Sort: command.Sort})
}
filters := entry.Filters{ContentTypeID: owner.ID, Unpaged: true, Sort: command.Sort}
if readScope == role.ScopeOwn {
filters.CreatedBy = &command.Caller.UserID
}
all, err := u.entries.ListPage(filters)
if err != nil {
return entry.PageResult{}, err
}
@@ -10,6 +10,6 @@ func NewListUsersUseCase(userRepository user.Repository) *ListUsersUseCase {
return &ListUsersUseCase{userRepository}
}
func (g *ListUsersUseCase) ListUsers() []user.User {
func (g *ListUsersUseCase) ListUsers() ([]user.User, error) {
return g.userRepository.GetAll()
}
@@ -21,8 +21,8 @@ type ListUsersAndInvitesUseCase struct {
}
type RoleAssigner struct {
ID uint32 `json:"id"`
Name string `json:"name"`
ID uint32
Name string
}
func newRoleAssigner(account user.User) *RoleAssigner {
@@ -89,7 +89,10 @@ func (g *ListUsersAndInvitesUseCase) ListUsersAndInvites() ([]DirectoryEntry, er
now := time.Now()
entries := []DirectoryEntry{}
users := g.userRepository.GetAll()
users, err := g.userRepository.GetAll()
if err != nil {
return nil, err
}
loaded := make(map[uint32]user.User, len(users))
for _, existingUser := range users {
loaded[existingUser.ID] = existingUser
@@ -98,7 +101,11 @@ func (g *ListUsersAndInvitesUseCase) ListUsersAndInvites() ([]DirectoryEntry, er
entries = append(entries, userEntry(existingUser, lookupRoleRef(refs, existingUser.RoleID), assignerAmong(loaded, existingUser.RoleAssignedByID)))
}
for _, existingInvite := range g.inviteRepository.GetAll() {
invites, err := g.inviteRepository.GetAll()
if err != nil {
return nil, err
}
for _, existingInvite := range invites {
status := existingInvite.StatusAt(now)
if status == invite.StatusAccepted {
continue
@@ -21,19 +21,19 @@ type TransferOwnershipCommand struct {
}
type OwnershipSuccessor struct {
ID uint32 `json:"id"`
Name string `json:"name"`
ID uint32
Name string
}
type OwnershipPredecessor struct {
ID uint32 `json:"id"`
Name string `json:"name"`
Role RoleRef `json:"role"`
ID uint32
Name string
Role RoleRef
}
type OwnershipTransfer struct {
Owner OwnershipSuccessor `json:"owner"`
FormerOwner OwnershipPredecessor `json:"former_owner"`
Owner OwnershipSuccessor
FormerOwner OwnershipPredecessor
}
func NewTransferOwnershipUseCase(users user.Repository, roles role.Repository, recorder audit.Recorder) *TransferOwnershipUseCase {
@@ -3,7 +3,11 @@ package useCases
import "RenewCMS/internal/domain/invite"
func findManageableInvite(inviteRepository invite.Repository, id uint32) (invite.Invite, error) {
for _, existing := range inviteRepository.GetAll() {
invites, err := inviteRepository.GetAll()
if err != nil {
return invite.Invite{}, err
}
for _, existing := range invites {
if existing.ID != id {
continue
}
+4 -4
View File
@@ -11,10 +11,10 @@ type InviteView struct {
}
type RoleRef struct {
ID uint32 `json:"id"`
Name string `json:"name"`
BuiltIn bool `json:"built_in"`
Locked bool `json:"locked"`
ID uint32
Name string
BuiltIn bool
Locked bool
}
func NewRoleRef(found role.Role) RoleRef {
+5 -5
View File
@@ -9,15 +9,15 @@ import (
)
type RoleEditor struct {
ID uint32 `json:"id"`
Name string `json:"name"`
ID uint32
Name string
}
type RoleView struct {
role.Role
Members int `json:"members"`
UpdatedAt *time.Time `json:"updated_at"`
UpdatedBy *RoleEditor `json:"updated_by"`
Members int
UpdatedAt *time.Time
UpdatedBy *RoleEditor
}
type roleViewer struct {