523 lines
14 KiB
Go
523 lines
14 KiB
Go
package bun
|
|
|
|
import (
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
|
"git.warky.dev/wdevs/relspecgo/pkg/readers"
|
|
)
|
|
|
|
func TestReader_ReadDatabase_Simple(t *testing.T) {
|
|
opts := &readers.ReaderOptions{
|
|
FilePath: filepath.Join("..", "..", "..", "tests", "assets", "bun", "simple.go"),
|
|
}
|
|
|
|
reader := NewReader(opts)
|
|
db, err := reader.ReadDatabase()
|
|
if err != nil {
|
|
t.Fatalf("ReadDatabase() error = %v", err)
|
|
}
|
|
|
|
if db == nil {
|
|
t.Fatal("ReadDatabase() returned nil database")
|
|
}
|
|
|
|
if len(db.Schemas) == 0 {
|
|
t.Fatal("Expected at least one schema")
|
|
}
|
|
|
|
schema := db.Schemas[0]
|
|
if schema.Name != "public" {
|
|
t.Errorf("Expected schema name 'public', got '%s'", schema.Name)
|
|
}
|
|
|
|
if len(schema.Tables) != 1 {
|
|
t.Fatalf("Expected 1 table, got %d", len(schema.Tables))
|
|
}
|
|
|
|
table := schema.Tables[0]
|
|
if table.Name != "users" {
|
|
t.Errorf("Expected table name 'users', got '%s'", table.Name)
|
|
}
|
|
|
|
if len(table.Columns) != 6 {
|
|
t.Errorf("Expected 6 columns, got %d", len(table.Columns))
|
|
}
|
|
|
|
// Verify id column - primary key should be NOT NULL
|
|
idCol, exists := table.Columns["id"]
|
|
if !exists {
|
|
t.Fatal("Column 'id' not found")
|
|
}
|
|
if !idCol.IsPrimaryKey {
|
|
t.Error("Column 'id' should be primary key")
|
|
}
|
|
if !idCol.AutoIncrement {
|
|
t.Error("Column 'id' should be auto-increment")
|
|
}
|
|
if !idCol.NotNull {
|
|
t.Error("Column 'id' should be NOT NULL (primary keys are always NOT NULL)")
|
|
}
|
|
if idCol.Type != "bigint" {
|
|
t.Errorf("Expected id type 'bigint', got '%s'", idCol.Type)
|
|
}
|
|
|
|
// Verify email column - explicit notnull tag should be NOT NULL
|
|
emailCol, exists := table.Columns["email"]
|
|
if !exists {
|
|
t.Fatal("Column 'email' not found")
|
|
}
|
|
if !emailCol.NotNull {
|
|
t.Error("Column 'email' should be NOT NULL (explicit 'notnull' tag)")
|
|
}
|
|
if emailCol.Type != "varchar" || emailCol.Length != 255 {
|
|
t.Errorf("Expected email type 'varchar(255)', got '%s' with length %d", emailCol.Type, emailCol.Length)
|
|
}
|
|
|
|
// Verify name column - primitive string type should be NOT NULL by default in Bun
|
|
nameCol, exists := table.Columns["name"]
|
|
if !exists {
|
|
t.Fatal("Column 'name' not found")
|
|
}
|
|
if !nameCol.NotNull {
|
|
t.Error("Column 'name' should be NOT NULL (primitive string type, no nullzero tag)")
|
|
}
|
|
if nameCol.Type != "text" {
|
|
t.Errorf("Expected name type 'text', got '%s'", nameCol.Type)
|
|
}
|
|
|
|
// Verify age column - pointer type should be nullable (NOT NULL = false)
|
|
ageCol, exists := table.Columns["age"]
|
|
if !exists {
|
|
t.Fatal("Column 'age' not found")
|
|
}
|
|
if ageCol.NotNull {
|
|
t.Error("Column 'age' should be nullable (pointer type *int)")
|
|
}
|
|
if ageCol.Type != "integer" {
|
|
t.Errorf("Expected age type 'integer', got '%s'", ageCol.Type)
|
|
}
|
|
|
|
// Verify is_active column - primitive bool type should be NOT NULL by default
|
|
isActiveCol, exists := table.Columns["is_active"]
|
|
if !exists {
|
|
t.Fatal("Column 'is_active' not found")
|
|
}
|
|
if !isActiveCol.NotNull {
|
|
t.Error("Column 'is_active' should be NOT NULL (primitive bool type, no nullzero tag)")
|
|
}
|
|
if isActiveCol.Type != "boolean" {
|
|
t.Errorf("Expected is_active type 'boolean', got '%s'", isActiveCol.Type)
|
|
}
|
|
|
|
// Verify created_at column - time.Time should be NOT NULL by default
|
|
createdAtCol, exists := table.Columns["created_at"]
|
|
if !exists {
|
|
t.Fatal("Column 'created_at' not found")
|
|
}
|
|
if !createdAtCol.NotNull {
|
|
t.Error("Column 'created_at' should be NOT NULL (time.Time is NOT NULL by default)")
|
|
}
|
|
if createdAtCol.Type != "timestamp" {
|
|
t.Errorf("Expected created_at type 'timestamp', got '%s'", createdAtCol.Type)
|
|
}
|
|
|
|
// Verify unique index on email
|
|
if len(table.Indexes) < 1 {
|
|
t.Error("Expected at least 1 index on users table")
|
|
}
|
|
}
|
|
|
|
func TestReader_ReadDatabase_Complex(t *testing.T) {
|
|
opts := &readers.ReaderOptions{
|
|
FilePath: filepath.Join("..", "..", "..", "tests", "assets", "bun", "complex.go"),
|
|
}
|
|
|
|
reader := NewReader(opts)
|
|
db, err := reader.ReadDatabase()
|
|
if err != nil {
|
|
t.Fatalf("ReadDatabase() error = %v", err)
|
|
}
|
|
|
|
if db == nil {
|
|
t.Fatal("ReadDatabase() returned nil database")
|
|
}
|
|
|
|
// Verify schema
|
|
if len(db.Schemas) != 1 {
|
|
t.Fatalf("Expected 1 schema, got %d", len(db.Schemas))
|
|
}
|
|
|
|
schema := db.Schemas[0]
|
|
if schema.Name != "public" {
|
|
t.Errorf("Expected schema name 'public', got '%s'", schema.Name)
|
|
}
|
|
|
|
// Verify tables
|
|
if len(schema.Tables) != 3 {
|
|
t.Fatalf("Expected 3 tables, got %d", len(schema.Tables))
|
|
}
|
|
|
|
// Find tables
|
|
var usersTable, postsTable, commentsTable *models.Table
|
|
for _, table := range schema.Tables {
|
|
switch table.Name {
|
|
case "users":
|
|
usersTable = table
|
|
case "posts":
|
|
postsTable = table
|
|
case "comments":
|
|
commentsTable = table
|
|
}
|
|
}
|
|
|
|
if usersTable == nil {
|
|
t.Fatal("Users table not found")
|
|
}
|
|
if postsTable == nil {
|
|
t.Fatal("Posts table not found")
|
|
}
|
|
if commentsTable == nil {
|
|
t.Fatal("Comments table not found")
|
|
}
|
|
|
|
// Verify users table - test NOT NULL logic for various field types
|
|
if len(usersTable.Columns) != 10 {
|
|
t.Errorf("Expected 10 columns in users table, got %d", len(usersTable.Columns))
|
|
}
|
|
|
|
// username - NOT NULL (explicit notnull tag)
|
|
usernameCol, exists := usersTable.Columns["username"]
|
|
if !exists {
|
|
t.Fatal("Column 'username' not found")
|
|
}
|
|
if !usernameCol.NotNull {
|
|
t.Error("Column 'username' should be NOT NULL (explicit 'notnull' tag)")
|
|
}
|
|
|
|
// first_name - nullable (pointer type)
|
|
firstNameCol, exists := usersTable.Columns["first_name"]
|
|
if !exists {
|
|
t.Fatal("Column 'first_name' not found")
|
|
}
|
|
if firstNameCol.NotNull {
|
|
t.Error("Column 'first_name' should be nullable (pointer type *string)")
|
|
}
|
|
|
|
// last_name - nullable (pointer type)
|
|
lastNameCol, exists := usersTable.Columns["last_name"]
|
|
if !exists {
|
|
t.Fatal("Column 'last_name' not found")
|
|
}
|
|
if lastNameCol.NotNull {
|
|
t.Error("Column 'last_name' should be nullable (pointer type *string)")
|
|
}
|
|
|
|
// bio - nullable (pointer type)
|
|
bioCol, exists := usersTable.Columns["bio"]
|
|
if !exists {
|
|
t.Fatal("Column 'bio' not found")
|
|
}
|
|
if bioCol.NotNull {
|
|
t.Error("Column 'bio' should be nullable (pointer type *string)")
|
|
}
|
|
|
|
// is_active - NOT NULL (primitive bool without nullzero)
|
|
isActiveCol, exists := usersTable.Columns["is_active"]
|
|
if !exists {
|
|
t.Fatal("Column 'is_active' not found")
|
|
}
|
|
if !isActiveCol.NotNull {
|
|
t.Error("Column 'is_active' should be NOT NULL (primitive bool type without nullzero)")
|
|
}
|
|
|
|
// Verify users table indexes
|
|
if len(usersTable.Indexes) < 1 {
|
|
t.Error("Expected at least 1 index on users table")
|
|
}
|
|
|
|
// Verify posts table
|
|
if len(postsTable.Columns) != 11 {
|
|
t.Errorf("Expected 11 columns in posts table, got %d", len(postsTable.Columns))
|
|
}
|
|
|
|
// excerpt - nullable (pointer type)
|
|
excerptCol, exists := postsTable.Columns["excerpt"]
|
|
if !exists {
|
|
t.Fatal("Column 'excerpt' not found")
|
|
}
|
|
if excerptCol.NotNull {
|
|
t.Error("Column 'excerpt' should be nullable (pointer type *string)")
|
|
}
|
|
|
|
// published - NOT NULL (primitive bool without nullzero)
|
|
publishedCol, exists := postsTable.Columns["published"]
|
|
if !exists {
|
|
t.Fatal("Column 'published' not found")
|
|
}
|
|
if !publishedCol.NotNull {
|
|
t.Error("Column 'published' should be NOT NULL (primitive bool type without nullzero)")
|
|
}
|
|
|
|
// published_at - nullable (has nullzero tag)
|
|
publishedAtCol, exists := postsTable.Columns["published_at"]
|
|
if !exists {
|
|
t.Fatal("Column 'published_at' not found")
|
|
}
|
|
if publishedAtCol.NotNull {
|
|
t.Error("Column 'published_at' should be nullable (has nullzero tag)")
|
|
}
|
|
|
|
// view_count - NOT NULL (primitive int64 without nullzero)
|
|
viewCountCol, exists := postsTable.Columns["view_count"]
|
|
if !exists {
|
|
t.Fatal("Column 'view_count' not found")
|
|
}
|
|
if !viewCountCol.NotNull {
|
|
t.Error("Column 'view_count' should be NOT NULL (primitive int64 type without nullzero)")
|
|
}
|
|
|
|
// Verify posts table indexes
|
|
if len(postsTable.Indexes) < 1 {
|
|
t.Error("Expected at least 1 index on posts table")
|
|
}
|
|
|
|
// Verify comments table
|
|
if len(commentsTable.Columns) != 6 {
|
|
t.Errorf("Expected 6 columns in comments table, got %d", len(commentsTable.Columns))
|
|
}
|
|
|
|
// user_id - nullable (pointer type)
|
|
userIDCol, exists := commentsTable.Columns["user_id"]
|
|
if !exists {
|
|
t.Fatal("Column 'user_id' not found in comments table")
|
|
}
|
|
if userIDCol.NotNull {
|
|
t.Error("Column 'user_id' should be nullable (pointer type *int64)")
|
|
}
|
|
|
|
// post_id - NOT NULL (explicit notnull tag)
|
|
postIDCol, exists := commentsTable.Columns["post_id"]
|
|
if !exists {
|
|
t.Fatal("Column 'post_id' not found in comments table")
|
|
}
|
|
if !postIDCol.NotNull {
|
|
t.Error("Column 'post_id' should be NOT NULL (explicit 'notnull' tag)")
|
|
}
|
|
|
|
// Verify foreign key constraints are created from relationship tags
|
|
// In Bun, relationships are defined with rel: tags
|
|
// The constraints should be created on the referenced tables
|
|
if len(postsTable.Constraints) > 0 {
|
|
// Check if FK constraint exists
|
|
var fkPostsUser *models.Constraint
|
|
for _, c := range postsTable.Constraints {
|
|
if c.Type == models.ForeignKeyConstraint && c.ReferencedTable == "users" {
|
|
fkPostsUser = c
|
|
break
|
|
}
|
|
}
|
|
|
|
if fkPostsUser != nil {
|
|
if len(fkPostsUser.Columns) != 1 || fkPostsUser.Columns[0] != "user_id" {
|
|
t.Error("Expected FK column 'user_id'")
|
|
}
|
|
if len(fkPostsUser.ReferencedColumns) != 1 || fkPostsUser.ReferencedColumns[0] != "id" {
|
|
t.Error("Expected FK referenced column 'id'")
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(commentsTable.Constraints) > 0 {
|
|
// Check if FK constraints exist
|
|
var fkCommentsPost, fkCommentsUser *models.Constraint
|
|
for _, c := range commentsTable.Constraints {
|
|
if c.Type == models.ForeignKeyConstraint {
|
|
if c.ReferencedTable == "posts" {
|
|
fkCommentsPost = c
|
|
} else if c.ReferencedTable == "users" {
|
|
fkCommentsUser = c
|
|
}
|
|
}
|
|
}
|
|
|
|
if fkCommentsPost != nil {
|
|
if len(fkCommentsPost.Columns) != 1 || fkCommentsPost.Columns[0] != "post_id" {
|
|
t.Error("Expected FK column 'post_id'")
|
|
}
|
|
}
|
|
|
|
if fkCommentsUser != nil {
|
|
if len(fkCommentsUser.Columns) != 1 || fkCommentsUser.Columns[0] != "user_id" {
|
|
t.Error("Expected FK column 'user_id'")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestReader_ReadSchema(t *testing.T) {
|
|
opts := &readers.ReaderOptions{
|
|
FilePath: filepath.Join("..", "..", "..", "tests", "assets", "bun", "simple.go"),
|
|
}
|
|
|
|
reader := NewReader(opts)
|
|
schema, err := reader.ReadSchema()
|
|
if err != nil {
|
|
t.Fatalf("ReadSchema() error = %v", err)
|
|
}
|
|
|
|
if schema == nil {
|
|
t.Fatal("ReadSchema() returned nil schema")
|
|
}
|
|
|
|
if schema.Name != "public" {
|
|
t.Errorf("Expected schema name 'public', got '%s'", schema.Name)
|
|
}
|
|
|
|
if len(schema.Tables) != 1 {
|
|
t.Errorf("Expected 1 table, got %d", len(schema.Tables))
|
|
}
|
|
}
|
|
|
|
func TestReader_ReadTable(t *testing.T) {
|
|
opts := &readers.ReaderOptions{
|
|
FilePath: filepath.Join("..", "..", "..", "tests", "assets", "bun", "simple.go"),
|
|
}
|
|
|
|
reader := NewReader(opts)
|
|
table, err := reader.ReadTable()
|
|
if err != nil {
|
|
t.Fatalf("ReadTable() error = %v", err)
|
|
}
|
|
|
|
if table == nil {
|
|
t.Fatal("ReadTable() returned nil table")
|
|
}
|
|
|
|
if table.Name != "users" {
|
|
t.Errorf("Expected table name 'users', got '%s'", table.Name)
|
|
}
|
|
|
|
if len(table.Columns) != 6 {
|
|
t.Errorf("Expected 6 columns, got %d", len(table.Columns))
|
|
}
|
|
}
|
|
|
|
func TestReader_ReadDatabase_Directory(t *testing.T) {
|
|
opts := &readers.ReaderOptions{
|
|
FilePath: filepath.Join("..", "..", "..", "tests", "assets", "bun"),
|
|
}
|
|
|
|
reader := NewReader(opts)
|
|
db, err := reader.ReadDatabase()
|
|
if err != nil {
|
|
t.Fatalf("ReadDatabase() error = %v", err)
|
|
}
|
|
|
|
if db == nil {
|
|
t.Fatal("ReadDatabase() returned nil database")
|
|
}
|
|
|
|
// Should read both simple.go and complex.go
|
|
if len(db.Schemas) == 0 {
|
|
t.Fatal("Expected at least one schema")
|
|
}
|
|
|
|
schema := db.Schemas[0]
|
|
// Should have at least 3 tables from complex.go (users, posts, comments)
|
|
// plus 1 from simple.go (users) - but same table name, so may be overwritten
|
|
if len(schema.Tables) < 3 {
|
|
t.Errorf("Expected at least 3 tables, got %d", len(schema.Tables))
|
|
}
|
|
}
|
|
|
|
func TestReader_ReadDatabase_InvalidPath(t *testing.T) {
|
|
opts := &readers.ReaderOptions{
|
|
FilePath: "/nonexistent/file.go",
|
|
}
|
|
|
|
reader := NewReader(opts)
|
|
_, err := reader.ReadDatabase()
|
|
if err == nil {
|
|
t.Error("Expected error for invalid file path")
|
|
}
|
|
}
|
|
|
|
func TestReader_ReadDatabase_EmptyPath(t *testing.T) {
|
|
opts := &readers.ReaderOptions{
|
|
FilePath: "",
|
|
}
|
|
|
|
reader := NewReader(opts)
|
|
_, err := reader.ReadDatabase()
|
|
if err == nil {
|
|
t.Error("Expected error for empty file path")
|
|
}
|
|
}
|
|
|
|
func TestReader_NullableTypes(t *testing.T) {
|
|
// This test specifically verifies the NOT NULL logic changes
|
|
opts := &readers.ReaderOptions{
|
|
FilePath: filepath.Join("..", "..", "..", "tests", "assets", "bun", "complex.go"),
|
|
}
|
|
|
|
reader := NewReader(opts)
|
|
db, err := reader.ReadDatabase()
|
|
if err != nil {
|
|
t.Fatalf("ReadDatabase() error = %v", err)
|
|
}
|
|
|
|
// Find posts table
|
|
var postsTable *models.Table
|
|
for _, schema := range db.Schemas {
|
|
for _, table := range schema.Tables {
|
|
if table.Name == "posts" {
|
|
postsTable = table
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if postsTable == nil {
|
|
t.Fatal("Posts table not found")
|
|
}
|
|
|
|
// Test all nullability scenarios
|
|
tests := []struct {
|
|
column string
|
|
notNull bool
|
|
reason string
|
|
}{
|
|
{"id", true, "primary key"},
|
|
{"user_id", true, "explicit notnull tag"},
|
|
{"title", true, "explicit notnull tag"},
|
|
{"slug", true, "explicit notnull tag"},
|
|
{"content", true, "explicit notnull tag"},
|
|
{"excerpt", false, "pointer type *string"},
|
|
{"published", true, "primitive bool without nullzero"},
|
|
{"view_count", true, "primitive int64 without nullzero"},
|
|
{"published_at", false, "has nullzero tag"},
|
|
{"created_at", true, "time.Time without nullzero"},
|
|
{"updated_at", true, "time.Time without nullzero"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
col, exists := postsTable.Columns[tt.column]
|
|
if !exists {
|
|
t.Errorf("Column '%s' not found", tt.column)
|
|
continue
|
|
}
|
|
|
|
if col.NotNull != tt.notNull {
|
|
if tt.notNull {
|
|
t.Errorf("Column '%s' should be NOT NULL (%s), but NotNull=%v",
|
|
tt.column, tt.reason, col.NotNull)
|
|
} else {
|
|
t.Errorf("Column '%s' should be nullable (%s), but NotNull=%v",
|
|
tt.column, tt.reason, col.NotNull)
|
|
}
|
|
}
|
|
}
|
|
}
|