feat: drop sqlite support
All checks were successful
Deploy / build-and-deploy (push) Successful in 1m27s
All checks were successful
Deploy / build-and-deploy (push) Successful in 1m27s
This commit is contained in:
parent
5e00060421
commit
da718427bd
27 changed files with 1296 additions and 115 deletions
9
internal/repository/main_test.go
Normal file
9
internal/repository/main_test.go
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) { testutil.PostgresMain(m) }
|
||||
124
internal/repository/space_audit_log_test.go
Normal file
124
internal/repository/space_audit_log_test.go
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func writeSpaceAuditLog(t *testing.T, repo SpaceAuditLogRepository, spaceID string, action model.SpaceAuditAction, actorID *string, metadata map[string]any, ts time.Time) *model.SpaceAuditLog {
|
||||
t.Helper()
|
||||
var meta []byte
|
||||
if metadata != nil {
|
||||
var err error
|
||||
meta, err = json.Marshal(metadata)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
entry := &model.SpaceAuditLog{
|
||||
ID: uuid.NewString(),
|
||||
SpaceID: spaceID,
|
||||
ActorID: actorID,
|
||||
Action: action,
|
||||
Metadata: meta,
|
||||
CreatedAt: ts,
|
||||
}
|
||||
require.NoError(t, repo.Create(entry))
|
||||
return entry
|
||||
}
|
||||
|
||||
func TestSpaceAuditLogRepository_CreateAndList(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewSpaceAuditLogRepository(dbi.DB)
|
||||
|
||||
actor := testutil.CreateTestUserWithName(t, dbi.DB, "audit-actor@example.com", strPtr("Actor Name"))
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, actor.ID, "Audit Space")
|
||||
|
||||
base := time.Now().Add(-time.Hour)
|
||||
writeSpaceAuditLog(t, repo, space.ID, model.SpaceAuditActionRenamed, &actor.ID, map[string]any{"old_name": "A", "new_name": "B"}, base)
|
||||
writeSpaceAuditLog(t, repo, space.ID, model.SpaceAuditActionMemberInvited, &actor.ID, nil, base.Add(10*time.Minute))
|
||||
writeSpaceAuditLog(t, repo, space.ID, model.SpaceAuditActionDeleted, &actor.ID, map[string]any{"space_name": "Audit Space"}, base.Add(20*time.Minute))
|
||||
|
||||
count, err := repo.CountBySpace(space.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 3, count)
|
||||
|
||||
logs, err := repo.ListBySpace(space.ID, 10, 0)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, logs, 3)
|
||||
|
||||
// Newest first.
|
||||
assert.Equal(t, model.SpaceAuditActionDeleted, logs[0].Action)
|
||||
assert.Equal(t, model.SpaceAuditActionMemberInvited, logs[1].Action)
|
||||
assert.Equal(t, model.SpaceAuditActionRenamed, logs[2].Action)
|
||||
|
||||
// Actor join populated.
|
||||
require.NotNil(t, logs[0].ActorName)
|
||||
assert.Equal(t, "Actor Name", *logs[0].ActorName)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSpaceAuditLogRepository_ListBySpace_Pagination(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewSpaceAuditLogRepository(dbi.DB)
|
||||
|
||||
actor := testutil.CreateTestUser(t, dbi.DB, "page@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, actor.ID, "Paged Space")
|
||||
|
||||
base := time.Now().Add(-time.Hour)
|
||||
for i := 0; i < 5; i++ {
|
||||
writeSpaceAuditLog(t, repo, space.ID, model.SpaceAuditActionRenamed, &actor.ID, nil, base.Add(time.Duration(i)*time.Minute))
|
||||
}
|
||||
|
||||
page1, err := repo.ListBySpace(space.ID, 2, 0)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, page1, 2)
|
||||
|
||||
page2, err := repo.ListBySpace(space.ID, 2, 2)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, page2, 2)
|
||||
|
||||
// No overlap between pages.
|
||||
assert.NotEqual(t, page1[0].ID, page2[0].ID)
|
||||
assert.NotEqual(t, page1[1].ID, page2[0].ID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSpaceAuditLogRepository_ListAccountEvents_FiltersByMetadata(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewSpaceAuditLogRepository(dbi.DB)
|
||||
|
||||
actor := testutil.CreateTestUser(t, dbi.DB, "acct-filter@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, actor.ID, "Filter Space")
|
||||
acct1 := testutil.CreateTestAccount(t, dbi.DB, space.ID, "Account 1")
|
||||
acct2 := testutil.CreateTestAccount(t, dbi.DB, space.ID, "Account 2")
|
||||
|
||||
base := time.Now().Add(-time.Hour)
|
||||
// Account 1 events
|
||||
writeSpaceAuditLog(t, repo, space.ID, model.SpaceAuditActionAccountCreated, &actor.ID, map[string]any{"account_id": acct1.ID, "account_name": "Account 1"}, base)
|
||||
writeSpaceAuditLog(t, repo, space.ID, model.SpaceAuditActionAccountRenamed, &actor.ID, map[string]any{"account_id": acct1.ID, "old_name": "Account 1", "new_name": "Renamed"}, base.Add(time.Minute))
|
||||
// Account 2 event
|
||||
writeSpaceAuditLog(t, repo, space.ID, model.SpaceAuditActionAccountCreated, &actor.ID, map[string]any{"account_id": acct2.ID, "account_name": "Account 2"}, base.Add(2*time.Minute))
|
||||
// Non-account event in same space — must NOT appear in account-scoped query
|
||||
writeSpaceAuditLog(t, repo, space.ID, model.SpaceAuditActionRenamed, &actor.ID, map[string]any{"old_name": "x", "new_name": "y"}, base.Add(3*time.Minute))
|
||||
|
||||
acct1Logs, err := repo.ListAccountEvents(acct1.ID, 10, 0)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, acct1Logs, 2)
|
||||
|
||||
acct1Count, err := repo.CountAccountEvents(acct1.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, acct1Count)
|
||||
|
||||
acct2Count, err := repo.CountAccountEvents(acct2.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, acct2Count)
|
||||
})
|
||||
}
|
||||
|
||||
func strPtr(s string) *string { return &s }
|
||||
140
internal/repository/transaction_audit_log_test.go
Normal file
140
internal/repository/transaction_audit_log_test.go
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func writeTxAuditLog(t *testing.T, repo TransactionAuditLogRepository, transactionID string, action model.TransactionAuditAction, actorID *string, metadata map[string]any, ts time.Time) *model.TransactionAuditLog {
|
||||
t.Helper()
|
||||
var meta []byte
|
||||
if metadata != nil {
|
||||
var err error
|
||||
meta, err = json.Marshal(metadata)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
entry := &model.TransactionAuditLog{
|
||||
ID: uuid.NewString(),
|
||||
TransactionID: transactionID,
|
||||
ActorID: actorID,
|
||||
Action: action,
|
||||
Metadata: meta,
|
||||
CreatedAt: ts,
|
||||
}
|
||||
require.NoError(t, repo.Create(entry))
|
||||
return entry
|
||||
}
|
||||
|
||||
func TestTransactionAuditLogRepository_CreateAndListByTransaction(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewTransactionAuditLogRepository(dbi.DB)
|
||||
|
||||
actor := testutil.CreateTestUserWithName(t, dbi.DB, "tx-audit@example.com", strPtr("Tx Actor"))
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, actor.ID, "Tx Audit Space")
|
||||
account := testutil.CreateTestAccount(t, dbi.DB, space.ID, "Acct")
|
||||
txn := testutil.CreateTestTransaction(t, dbi.DB, account.ID, "Coffee", model.TransactionTypeWithdrawal, decimal.NewFromInt(5))
|
||||
|
||||
base := time.Now().Add(-time.Hour)
|
||||
writeTxAuditLog(t, repo, txn.ID, model.TransactionAuditActionCreated, &actor.ID,
|
||||
map[string]any{"account_id": account.ID, "transaction_type": "withdrawal", "title": "Coffee", "amount": "5.00"}, base)
|
||||
writeTxAuditLog(t, repo, txn.ID, model.TransactionAuditActionEdited, &actor.ID,
|
||||
map[string]any{"account_id": account.ID, "changes": map[string]any{"title": map[string]any{"old": "Coffee", "new": "Latte"}}}, base.Add(time.Minute))
|
||||
|
||||
count, err := repo.CountByTransaction(txn.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, count)
|
||||
|
||||
logs, err := repo.ListByTransaction(txn.ID, 10, 0)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, logs, 2)
|
||||
// Newest first.
|
||||
assert.Equal(t, model.TransactionAuditActionEdited, logs[0].Action)
|
||||
require.NotNil(t, logs[0].ActorName)
|
||||
assert.Equal(t, "Tx Actor", *logs[0].ActorName)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTransactionAuditLogRepository_ListByAccount_LiveAndDeletedFallback(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewTransactionAuditLogRepository(dbi.DB)
|
||||
|
||||
actor := testutil.CreateTestUser(t, dbi.DB, "acct-list@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, actor.ID, "Acct List Space")
|
||||
account := testutil.CreateTestAccount(t, dbi.DB, space.ID, "Acct")
|
||||
|
||||
// Live transaction with audit entry
|
||||
live := testutil.CreateTestTransaction(t, dbi.DB, account.ID, "Live", model.TransactionTypeDeposit, decimal.NewFromInt(10))
|
||||
writeTxAuditLog(t, repo, live.ID, model.TransactionAuditActionCreated, &actor.ID,
|
||||
map[string]any{"account_id": account.ID}, time.Now().Add(-2*time.Minute))
|
||||
|
||||
// Audit entry referencing a transaction that no longer exists.
|
||||
// Resolution must fall back to metadata.account_id.
|
||||
ghostID := uuid.NewString()
|
||||
writeTxAuditLog(t, repo, ghostID, model.TransactionAuditActionDeleted, &actor.ID,
|
||||
map[string]any{"account_id": account.ID, "title": "Ghost"}, time.Now().Add(-time.Minute))
|
||||
|
||||
// Audit entry for a different account — must not appear.
|
||||
other := testutil.CreateTestAccount(t, dbi.DB, space.ID, "Other")
|
||||
otherTxn := testutil.CreateTestTransaction(t, dbi.DB, other.ID, "Other", model.TransactionTypeDeposit, decimal.NewFromInt(1))
|
||||
writeTxAuditLog(t, repo, otherTxn.ID, model.TransactionAuditActionCreated, &actor.ID,
|
||||
map[string]any{"account_id": other.ID}, time.Now())
|
||||
|
||||
count, err := repo.CountByAccount(account.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, count, "should count live + ghost-via-metadata")
|
||||
|
||||
logs, err := repo.ListByAccount(account.ID, 10, 0)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, logs, 2)
|
||||
// Confirm both kinds present (one live, one ghost).
|
||||
ids := []string{logs[0].TransactionID, logs[1].TransactionID}
|
||||
assert.Contains(t, ids, live.ID)
|
||||
assert.Contains(t, ids, ghostID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTransactionAuditLogRepository_ListBySpace(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewTransactionAuditLogRepository(dbi.DB)
|
||||
|
||||
actor := testutil.CreateTestUser(t, dbi.DB, "space-list@example.com", nil)
|
||||
|
||||
// Two spaces, each with an account and a transaction.
|
||||
spaceA := testutil.CreateTestSpace(t, dbi.DB, actor.ID, "Space A")
|
||||
acctA := testutil.CreateTestAccount(t, dbi.DB, spaceA.ID, "Acct A")
|
||||
txnA := testutil.CreateTestTransaction(t, dbi.DB, acctA.ID, "txnA", model.TransactionTypeDeposit, decimal.NewFromInt(1))
|
||||
writeTxAuditLog(t, repo, txnA.ID, model.TransactionAuditActionCreated, &actor.ID,
|
||||
map[string]any{"account_id": acctA.ID}, time.Now().Add(-time.Minute))
|
||||
|
||||
spaceB := testutil.CreateTestSpace(t, dbi.DB, actor.ID, "Space B")
|
||||
acctB := testutil.CreateTestAccount(t, dbi.DB, spaceB.ID, "Acct B")
|
||||
txnB := testutil.CreateTestTransaction(t, dbi.DB, acctB.ID, "txnB", model.TransactionTypeDeposit, decimal.NewFromInt(1))
|
||||
writeTxAuditLog(t, repo, txnB.ID, model.TransactionAuditActionCreated, &actor.ID,
|
||||
map[string]any{"account_id": acctB.ID}, time.Now())
|
||||
|
||||
// Ghost in space A (deleted txn).
|
||||
ghostID := uuid.NewString()
|
||||
writeTxAuditLog(t, repo, ghostID, model.TransactionAuditActionDeleted, &actor.ID,
|
||||
map[string]any{"account_id": acctA.ID}, time.Now().Add(-30*time.Second))
|
||||
|
||||
countA, err := repo.CountBySpace(spaceA.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, countA)
|
||||
|
||||
countB, err := repo.CountBySpace(spaceB.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, countB)
|
||||
|
||||
logsA, err := repo.ListBySpace(spaceA.ID, 10, 0)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, logsA, 2)
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue