restructure project, add claudemd

This commit is contained in:
2026-07-08 16:36:17 -04:00
parent a7964f9410
commit 2a5fbffaa2
315 changed files with 81075 additions and 0 deletions

View File

@@ -0,0 +1,507 @@
package dbutil
import (
"reflect"
"testing"
"time"
"github.com/DATA-DOG/go-sqlmock"
"github.com/google/uuid"
)
// buildMapping (flat struct)
func TestBuildMappingFlat(t *testing.T) {
tm := getMapping(reflect.TypeOf(testUser{}))
want := map[string]bool{
"id": true, "username": true, "email": true, "password": true,
"first_name": true, "last_name": true,
"login_count": true, "created": true, "active": true,
}
for col := range want {
if _, ok := tm.columns[col]; !ok {
t.Errorf("missing mapping for column %q", col)
}
}
if len(tm.columns) != len(want) {
t.Errorf("column count = %d, want %d", len(tm.columns), len(want))
}
}
// buildMapping (nested struct / DTO)
func TestBuildMappingNested(t *testing.T) {
type DTO struct {
Membership testMembership
User testUser
}
tm := getMapping(reflect.TypeOf(DTO{}))
if _, ok := tm.columns["membership.id"]; !ok {
t.Error("missing membership.id")
}
if _, ok := tm.columns["membership.user_id"]; !ok {
t.Error("missing membership.user_id")
}
if _, ok := tm.columns["user.id"]; !ok {
t.Error("missing user.id")
}
if _, ok := tm.columns["user.username"]; !ok {
t.Error("missing user.username")
}
// No unprefixed columns should exist
if _, ok := tm.columns["id"]; ok {
t.Error("should not have unprefixed 'id'")
}
}
// buildMapping (alias tag)
func TestBuildMappingAlias(t *testing.T) {
type DTO struct {
User testUser
CreatedBy testUser `alias:"created_by"`
}
tm := getMapping(reflect.TypeOf(DTO{}))
if _, ok := tm.columns["user.id"]; !ok {
t.Error("missing user.id")
}
if _, ok := tm.columns["created_by.id"]; !ok {
t.Error("missing created_by.id")
}
if _, ok := tm.columns["created_by.username"]; !ok {
t.Error("missing created_by.username")
}
}
// buildMapping (pointer-to-struct for LEFT JOINs)
func TestBuildMappingPointerStruct(t *testing.T) {
type DTO struct {
Session testSession
User *testUser `alias:"test_user"`
}
tm := getMapping(reflect.TypeOf(DTO{}))
if _, ok := tm.columns["session.id"]; !ok {
t.Error("missing session.id")
}
if _, ok := tm.columns["test_user.id"]; !ok {
t.Error("missing test_user.id (via alias tag)")
}
if len(tm.ptrStructs) != 1 {
t.Errorf("ptrStructs len = %d, want 1", len(tm.ptrStructs))
}
}
// buildMapping (embedded struct)
func TestBuildMappingEmbedded(t *testing.T) {
type Base struct {
ID uuid.UUID `db:"id"`
Created time.Time `db:"created"`
}
type Extended struct {
Base
Name string `db:"name"`
}
tm := getMapping(reflect.TypeOf(Extended{}))
if _, ok := tm.columns["id"]; !ok {
t.Error("missing flattened id from embedded Base")
}
if _, ok := tm.columns["created"]; !ok {
t.Error("missing flattened created from embedded Base")
}
if _, ok := tm.columns["name"]; !ok {
t.Error("missing name")
}
}
// isModelStruct
func TestIsModelStruct(t *testing.T) {
if !isModelStruct(reflect.TypeOf(testUser{})) {
t.Error("testUser should be a model struct")
}
if isModelStruct(reflect.TypeOf(time.Time{})) {
t.Error("time.Time should not be a model struct")
}
if isModelStruct(reflect.TypeOf(struct{ X int }{})) {
t.Error("anonymous struct without db tags should not be a model struct")
}
}
// DebugMapping
func TestDebugMapping(t *testing.T) {
type DTO struct {
User testUser
}
m := DebugMapping(DTO{})
if path, ok := m["user.id"]; !ok || path != "User.ID" {
t.Errorf("user.id mapping = %q, ok = %v", path, ok)
}
if path, ok := m["user.username"]; !ok || path != "User.Username" {
t.Errorf("user.username mapping = %q, ok = %v", path, ok)
}
}
// ScanOne (flat struct via sqlmock)
func TestScanOneFlat(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatal(err)
}
defer db.Close()
id := uuid.New()
now := time.Now().Truncate(time.Second)
rows := sqlmock.NewRows([]string{"id", "username", "email", "password", "first_name", "last_name", "login_count", "created", "active"}).
AddRow(id, "alice", "alice@test.com", "hash", "Alice", "Smith", int32(5), now, true)
mock.ExpectQuery("SELECT").WillReturnRows(rows)
sqlRows, err := db.Query("SELECT anything")
if err != nil {
t.Fatal(err)
}
defer sqlRows.Close()
var user testUser
err = ScanOne(sqlRows, &user)
if err != nil {
t.Fatal(err)
}
if user.ID != id {
t.Errorf("ID = %v, want %v", user.ID, id)
}
if user.Username != "alice" {
t.Errorf("Username = %q", user.Username)
}
if user.Email != "alice@test.com" {
t.Errorf("Email = %q", user.Email)
}
if user.FirstName != "Alice" {
t.Errorf("FirstName = %q", user.FirstName)
}
if user.LoginCount != 5 {
t.Errorf("LoginCount = %d", user.LoginCount)
}
if user.Active != true {
t.Errorf("Active = %v", user.Active)
}
}
// ScanAll (multiple rows)
func TestScanAllFlat(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatal(err)
}
defer db.Close()
id1, id2 := uuid.New(), uuid.New()
now := time.Now().Truncate(time.Second)
rows := sqlmock.NewRows([]string{"id", "key", "user_id", "org_id", "created", "user_agent", "revoked"}).
AddRow(id1, "key1", uuid.New(), nil, now, "Mozilla", false).
AddRow(id2, "key2", uuid.New(), nil, now, "Chrome", true)
mock.ExpectQuery("SELECT").WillReturnRows(rows)
sqlRows, err := db.Query("SELECT anything")
if err != nil {
t.Fatal(err)
}
defer sqlRows.Close()
var sessions []testSession
err = ScanAll(sqlRows, &sessions)
if err != nil {
t.Fatal(err)
}
if len(sessions) != 2 {
t.Fatalf("len = %d, want 2", len(sessions))
}
if sessions[0].Key != "key1" {
t.Errorf("[0].Key = %q", sessions[0].Key)
}
if sessions[1].Revoked != true {
t.Errorf("[1].Revoked = %v", sessions[1].Revoked)
}
}
// ScanOne (nested DTO with prefixed columns)
func TestScanOneNested(t *testing.T) {
type DTO struct {
Membership testMembership
User testUser
}
db, mock, err := sqlmock.New()
if err != nil {
t.Fatal(err)
}
defer db.Close()
mID, uID, orgID := uuid.New(), uuid.New(), uuid.New()
now := time.Now().Truncate(time.Second)
cols := []string{
"membership.id", "membership.user_id", "membership.org_id",
"membership.created_by", "membership.joined",
"membership.login_count",
"user.id", "user.username", "user.email",
}
rows := sqlmock.NewRows(cols).
AddRow(
mID, uID, orgID,
uuid.New(), now,
int32(10),
uID, "alice", "alice@test.com",
)
mock.ExpectQuery("SELECT").WillReturnRows(rows)
sqlRows, err := db.Query("SELECT anything")
if err != nil {
t.Fatal(err)
}
defer sqlRows.Close()
var dto DTO
err = ScanOne(sqlRows, &dto)
if err != nil {
t.Fatal(err)
}
if dto.Membership.ID != mID {
t.Errorf("Membership.ID = %v, want %v", dto.Membership.ID, mID)
}
if dto.Membership.OrgID != orgID {
t.Errorf("Membership.OrgID = %v, want %v", dto.Membership.OrgID, orgID)
}
if dto.User.Username != "alice" {
t.Errorf("User.Username = %q", dto.User.Username)
}
if dto.Membership.LoginCount != 10 {
t.Errorf("Membership.LoginCount = %v, want 10", dto.Membership.LoginCount)
}
}
// ScanOne (alias tag)
func TestScanOneAlias(t *testing.T) {
type DTO struct {
User testUser
CreatedBy testUser `alias:"created_by"`
}
db, mock, err := sqlmock.New()
if err != nil {
t.Fatal(err)
}
defer db.Close()
userID, cbID := uuid.New(), uuid.New()
cols := []string{"user.id", "user.username", "created_by.id", "created_by.username"}
rows := sqlmock.NewRows(cols).
AddRow(userID, "alice", cbID, "bob")
mock.ExpectQuery("SELECT").WillReturnRows(rows)
sqlRows, err := db.Query("SELECT anything")
if err != nil {
t.Fatal(err)
}
defer sqlRows.Close()
var dto DTO
err = ScanOne(sqlRows, &dto)
if err != nil {
t.Fatal(err)
}
if dto.User.ID != userID {
t.Errorf("User.ID = %v, want %v", dto.User.ID, userID)
}
if dto.User.Username != "alice" {
t.Errorf("User.Username = %q", dto.User.Username)
}
if dto.CreatedBy.ID != cbID {
t.Errorf("CreatedBy.ID = %v, want %v", dto.CreatedBy.ID, cbID)
}
if dto.CreatedBy.Username != "bob" {
t.Errorf("CreatedBy.Username = %q", dto.CreatedBy.Username)
}
}
// Pointer-to-struct nil detection (LEFT JOIN)
func TestScanOnePtrStructNilDetection(t *testing.T) {
type DTO struct {
Session testSession
User *testUser `alias:"test_user"`
}
db, mock, err := sqlmock.New()
if err != nil {
t.Fatal(err)
}
defer db.Close()
sessID := uuid.New()
now := time.Now().Truncate(time.Second)
cols := []string{
"session.id", "session.key", "session.user_id", "session.org_id",
"session.created", "session.user_agent", "session.revoked",
// All user columns are NULL (LEFT JOIN miss)
"test_user.id", "test_user.username",
}
rows := sqlmock.NewRows(cols).
AddRow(
sessID, "key1", uuid.New(), nil,
now, "Mozilla", false,
// NULL user
uuid.Nil, "",
)
mock.ExpectQuery("SELECT").WillReturnRows(rows)
sqlRows, err := db.Query("SELECT anything")
if err != nil {
t.Fatal(err)
}
defer sqlRows.Close()
var dto DTO
err = ScanOne(sqlRows, &dto)
if err != nil {
t.Fatal(err)
}
if dto.Session.ID != sessID {
t.Errorf("Session.ID = %v, want %v", dto.Session.ID, sessID)
}
if dto.User != nil {
t.Errorf("User should be nil for LEFT JOIN miss, got %+v", dto.User)
}
}
func TestScanOnePtrStructNonNil(t *testing.T) {
type DTO struct {
Session testSession
User *testUser `alias:"test_user"`
}
db, mock, err := sqlmock.New()
if err != nil {
t.Fatal(err)
}
defer db.Close()
sessID, userID := uuid.New(), uuid.New()
now := time.Now().Truncate(time.Second)
cols := []string{
"session.id", "session.key", "session.user_id", "session.org_id",
"session.created", "session.user_agent", "session.revoked",
"test_user.id", "test_user.username",
}
rows := sqlmock.NewRows(cols).
AddRow(
sessID, "key1", userID, nil,
now, "Mozilla", false,
userID, "alice",
)
mock.ExpectQuery("SELECT").WillReturnRows(rows)
sqlRows, err := db.Query("SELECT anything")
if err != nil {
t.Fatal(err)
}
defer sqlRows.Close()
var dto DTO
err = ScanOne(sqlRows, &dto)
if err != nil {
t.Fatal(err)
}
if dto.User == nil {
t.Fatal("User should not be nil")
}
if dto.User.ID != userID {
t.Errorf("User.ID = %v, want %v", dto.User.ID, userID)
}
if dto.User.Username != "alice" {
t.Errorf("User.Username = %q", dto.User.Username)
}
}
// ScanOne: sql.ErrNoRows
func TestScanOneNoRows(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatal(err)
}
defer db.Close()
rows := sqlmock.NewRows([]string{"id", "username"})
mock.ExpectQuery("SELECT").WillReturnRows(rows)
sqlRows, err := db.Query("SELECT anything")
if err != nil {
t.Fatal(err)
}
defer sqlRows.Close()
type Small struct {
ID string `db:"id"`
Username string `db:"username"`
}
var s Small
err = ScanOne(sqlRows, &s)
if err == nil {
t.Error("expected error for no rows")
}
}
// Columns function
func TestColumnsFunction(t *testing.T) {
result := Columns(testSession{}, "s")
if !containsSubstr(result, `s.id AS "test_session.id"`) {
t.Errorf("missing s.id alias: %s", result)
}
if !containsSubstr(result, `s.key AS "test_session.key"`) {
t.Errorf("missing s.key alias: %s", result)
}
}
func TestColumnsWithPrefix(t *testing.T) {
result := Columns(testUser{}, "cb", "created_by")
if !containsSubstr(result, `cb.id AS "created_by.id"`) {
t.Errorf("missing custom prefix alias: %s", result)
}
}