Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions examples/01-basic-crud/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -449,17 +449,12 @@ fabrica generate
# 6. Update dependencies
go mod tidy

# 7. Uncomment in cmd/server/main.go:
# - import "github.com/user/device-inventory/internal/storage"
# - storage.InitFileBackend("./data")
# - RegisterGeneratedRoutes(r)

# 8. Build server and client
# 7. Build server and client
go build -o server ./cmd/server
fabrica generate --client
go build -o client ./cmd/client

# 9. Run and test
# 8. Run and test
./server # In one terminal
./client device list # In another terminal

Expand Down
94 changes: 93 additions & 1 deletion test/integration/clean_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,99 @@ func (s *FabricaTestSuite) TestCreateFRUApplication() {

// For file storage, check storage file instead of Ent schema
project.AssertFileExists("internal/storage/storage_generated.go")
} // Run the test suite
}

func (s *FabricaTestSuite) TestExample1_EndToEnd() {
// Create project
project := s.createProject("example1-crud", "github.com/test/example1", "file")

// 1. Initialize project
err := project.Initialize(s.fabricaBinary)
s.Require().NoError(err, "project initialization should succeed")

// 2. Add resource
err = project.AddResource(s.fabricaBinary, "Device")
s.Require().NoError(err, "adding resource should succeed")

// 3. Customize resource (New Step)
err = project.Example1_CustomizeResource()
s.Require().NoError(err, "customizing resource should succeed")

// 4. Generate code
err = project.Generate(s.fabricaBinary)
s.Require().NoError(err, "code generation should succeed")

// 5. Configure server (New Step)
err = project.Example1_ConfigureServer()
s.Require().NoError(err, "configuring server main.go should succeed")

// 6. Build project
err = project.Build()
s.Require().NoError(err, "project should build successfully")

// 7. Start server
err = project.StartServer()
s.Require().NoError(err, "server should start successfully")
// Ensure server is stopped when test finishes
s.T().Cleanup(func() {
project.StopServer()
})

// 8. Run Client Tests (Full CRUD)

// CREATE
createSpec := map[string]interface{}{
"description": "Core network switch",
"ipAddress": "192.168.1.10",
"location": "DataCenter A",
"rack": "R42",
}
created, err := project.CreateResource("device", createSpec)
s.Require().NoError(err, "client create should succeed")
s.Require().NotNil(created, "created resource should not be nil")

// Verify metadata and spec
uid, ok := created["metadata"].(map[string]interface{})["uid"].(string)
s.Require().True(ok, "should get uid from metadata")
s.Require().NotEmpty(uid, "uid should not be empty")
project.AssertResourceHasSpec(s.T(), created, createSpec)

// LIST
listed, err := project.ListResources("device")
s.Require().NoError(err, "client list should succeed")
s.Require().Len(listed, 1, "list should return one device")
s.Require().Equal(uid, listed[0]["metadata"].(map[string]interface{})["uid"].(string))

// GET
got, err := project.GetResource("device", uid)
s.Require().NoError(err, "client get should succeed")
project.AssertResourceHasSpec(s.T(), got, createSpec)

// PATCH (Example 1 doesn't test this, but we can use the helper)
patchSpec := map[string]interface{}{
"location": "DataCenter B",
}
patched, err := project.PatchResource("device", uid, patchSpec)
s.Require().NoError(err, "client patch should succeed")

// Verify patch
s.Require().Equal("DataCenter B", patched["spec"].(map[string]interface{})["location"], "location should be updated")
s.Require().Equal("192.168.1.10", patched["spec"].(map[string]interface{})["ipAddress"], "ipAddress should be unchanged")

// DELETE
err = project.DeleteResource("device", uid)
s.Require().NoError(err, "client delete should succeed")

// VERIFY DELETE
listedAfterDelete, err := project.ListResources("device")
s.Require().NoError(err, "client list after delete should succeed")
s.Require().Len(listedAfterDelete, 0, "list should be empty after delete")

_, err = project.GetResource("device", uid)
s.Require().Error(err, "client get after delete should fail")
}

// Run the test suite
func TestFabricaTestSuite(t *testing.T) {
suite.Run(t, new(FabricaTestSuite))
}
94 changes: 93 additions & 1 deletion test/integration/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"os/exec"
"path/filepath"
"time"
"strings"

"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
Expand Down Expand Up @@ -310,7 +311,7 @@ func (p *TestProject) PatchResource(resourceName, id string, patch interface{})
patchJSON = string(patchBytes)
}

output, err := p.RunClient(resourceName, "patch", id, "--patch", patchJSON)
output, err := p.RunClient(resourceName, "patch", id, "--spec", patchJSON)
if err != nil {
return nil, fmt.Errorf("patch failed: %w\nOutput: %s", err, output)
}
Expand Down Expand Up @@ -349,3 +350,94 @@ func (p *TestProject) AssertResourceHasSpec(t require.TestingT, resource map[str
require.Equal(t, expectedValue, actualValue, "spec[%s] should match expected value", key)
}
}

// ModifyFile reads a file, applies a modification function, and writes it back
func (p *TestProject) ModifyFile(relativePath string, modifier func(string) string) error {
path := filepath.Join(p.Dir, relativePath)
content, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("failed to read file %s: %w", path, err)
}

newContent := modifier(string(content))

if err := os.WriteFile(path, []byte(newContent), 0644); err != nil {
return fmt.Errorf("failed to write file %s: %w", path, err)
}
return nil
}

// Example1_CustomizeResource updates the Device spec as per Example 1
func (p *TestProject) Example1_CustomizeResource() error {
// Path: pkg/resources/device/device.go
relPath := filepath.Join("pkg", "resources", "device", "device.go")

return p.ModifyFile(relPath, func(content string) string {
// We replace the default placeholder or the simple struct definition
// with the full definition from the example
target := `type DeviceSpec struct {
Description string ` + "`json:\"description,omitempty\" validate:\"max=200\"`" + `
// Add your spec fields here
}`

replacement := `type DeviceSpec struct {
Description string ` + "`json:\"description,omitempty\" validate:\"max=200\"`" + `
IPAddress string ` + "`json:\"ipAddress,omitempty\" validate:\"omitempty,ip\"`" + `
Location string ` + "`json:\"location,omitempty\"`" + `
Rack string ` + "`json:\"rack,omitempty\"`" + `
}`
// Try specific replacement first
if strings.Contains(content, target) {
return strings.Replace(content, target, replacement, 1)
}

// Fallback: If formatting is slightly different, try to inject just the fields
// This assumes the file contains "// Add your spec fields here"
fields := `IPAddress string ` + "`json:\"ipAddress,omitempty\" validate:\"omitempty,ip\"`" + `
Location string ` + "`json:\"location,omitempty\"`" + `
Rack string ` + "`json:\"rack,omitempty\"`"

return strings.Replace(content, "// Add your spec fields here", fields, 1)
})
}

// Example1_ConfigureServer uncomments the storage and route registration in main.go
func (p *TestProject) Example1_ConfigureServer() error {
relPath := filepath.Join("cmd", "server", "main.go")

return p.ModifyFile(relPath, func(content string) string {
// 1. Uncomment the storage import
// Expecting: // "github.com/user/device-inventory/internal/storage"
// We need to be careful to match the actual module name or just the suffix
lines := strings.Split(content, "\n")
var newLines []string

for _, line := range lines {
trimmed := strings.TrimSpace(line)

// Uncomment import for storage
if strings.HasPrefix(trimmed, "//") && strings.Contains(trimmed, "/internal/storage\"") {
line = strings.Replace(line, "// ", "", 1)
line = strings.Replace(line, "//", "", 1) // Handle case without space
}

// Uncomment storage init
// Expecting: // storage.InitFileBackend("./data")
if strings.HasPrefix(trimmed, "//") && strings.Contains(trimmed, "storage.InitFileBackend") {
line = strings.Replace(line, "// ", "", 1)
line = strings.Replace(line, "//", "", 1)
}

// Uncomment route registration
// Expecting: // RegisterGeneratedRoutes(r)
if strings.HasPrefix(trimmed, "//") && strings.Contains(trimmed, "RegisterGeneratedRoutes") {
line = strings.Replace(line, "// ", "", 1)
line = strings.Replace(line, "//", "", 1)
}

newLines = append(newLines, line)
}

return strings.Join(newLines, "\n")
})
}