README
¶
Keyline End-to-End (E2E) Tests
End-to-end tests validate the complete Keyline system by running the actual API server and making real HTTP requests. These tests ensure that all components work together correctly in a production-like environment.
Overview
The E2E test suite uses:
- Ginkgo: BDD-style testing framework
- Gomega: Matcher/assertion library
- Test Harness: Custom test infrastructure for isolated test environments
- Keyline API Client: Type-safe Go client for API interactions
Architecture
Test Components
-
Test Harness (
harness.go)- Creates isolated test environments for each test suite
- Manages database lifecycle (create, migrate, cleanup)
- Starts the API server on a unique port
- Provides a configured API client
- Handles time mocking for time-dependent tests
-
Test Suite (
suite_test.go)- Entry point for the test runner
- Initializes logging and Ginkgo/Gomega
-
Test Specs (e.g.,
application_flow_test.go)- Individual test scenarios using BDD style
- Organized with
Describe,It,BeforeAll,AfterAllblocks
Test Isolation
Each test suite gets:
- Unique database: Randomly named PostgreSQL database for complete isolation
- Unique server port: Avoids port conflicts when running tests in parallel
- Fresh state: No data pollution between test suites
- Clean shutdown: Automatic cleanup after tests complete
Prerequisites
Before running E2E tests, ensure the following services are running:
# Start dependencies with Docker Compose
docker compose up -d
# Or start PostgreSQL manually
# PostgreSQL should be running on localhost:5732
# Default credentials: user/password
Running E2E Tests
Using Just (Recommended)
# Run all E2E tests
just e2e
# Run full CI pipeline (includes E2E tests)
just ci
Using Go Test
# Run E2E tests with the e2e build tag
go test -tags=e2e ./tests/e2e/...
# Run with race detector
go test -race -tags=e2e ./tests/e2e/...
# Run with verbose output
go test -v -tags=e2e ./tests/e2e/...
# Run specific test
go test -tags=e2e -run "Application flow" ./tests/e2e/...
Using Ginkgo CLI
# Install Ginkgo CLI
go install github.com/onsi/ginkgo/v2/ginkgo
# Run tests
ginkgo -tags=e2e ./tests/e2e/
# Run with focus (only focused specs)
ginkgo -tags=e2e --focus "creates application" ./tests/e2e/
# Run in parallel (experimental - requires careful test isolation)
ginkgo -tags=e2e -p ./tests/e2e/
Writing E2E Tests
Basic Test Structure
package e2e
import (
"Keyline/internal/handlers"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("Feature Name", Ordered, func() {
var h *harness
BeforeAll(func() {
// Create test harness once for all tests in this suite
h = newE2eTestHarness()
})
AfterAll(func() {
// Clean up after all tests
h.Close()
})
It("does something", func() {
// Your test code here
})
It("does something else", func() {
// Another test
})
})
Example: Testing Application Creation
var _ = Describe("Application Management", Ordered, func() {
var h *harness
BeforeAll(func() {
h = newE2eTestHarness()
})
AfterAll(func() {
h.Close()
})
It("creates a public application", func() {
// Arrange
createDto := handlers.CreateApplicationRequestDto{
Name: "test-app",
DisplayName: "Test Application",
RedirectUris: []string{"http://localhost:3000/callback"},
PostLogoutUris: []string{"http://localhost:3000/logout"},
Type: "public",
}
// Act
app, err := h.Client().Application().Create(h.Ctx(), createDto)
// Assert
Expect(err).ToNot(HaveOccurred())
Expect(app.Id).ToNot(BeEmpty())
Expect(app.Secret).To(BeNil()) // Public apps don't have secrets
})
It("creates a confidential application with secret", func() {
createDto := handlers.CreateApplicationRequestDto{
Name: "confidential-app",
DisplayName: "Confidential Application",
RedirectUris: []string{"http://localhost:3000/callback"},
PostLogoutUris: []string{"http://localhost:3000/logout"},
Type: "confidential",
}
app, err := h.Client().Application().Create(h.Ctx(), createDto)
Expect(err).ToNot(HaveOccurred())
Expect(app.Id).ToNot(BeEmpty())
Expect(app.Secret).ToNot(BeNil()) // Confidential apps have secrets
Expect(*app.Secret).ToNot(BeEmpty())
})
It("rejects unauthorized requests", func() {
createDto := handlers.CreateApplicationRequestDto{
Name: "test-app",
DisplayName: "Test Application",
RedirectUris: []string{"http://localhost:3000/callback"},
PostLogoutUris: []string{"http://localhost:3000/logout"},
Type: "public",
}
_, err := h.Client().Application().Create(h.Ctx(), createDto)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("401 Unauthorized"))
})
})
Using the Test Harness
The test harness provides several useful methods and properties:
// Get the API client
client := h.Client()
// Get the context (with authentication and scope)
ctx := h.Ctx()
// Get the virtual server name
vs := h.VirtualServer() // Returns "test-vs"
// Set the current time (for time-dependent tests)
h.SetTime(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
// Clean up resources (called automatically in AfterAll)
h.Close()
Testing with Authentication
Testing with authentication is a WIP.
Testing Time-Dependent Features
It("handles token expiration", func() {
// Set initial time
initialTime := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
h.SetTime(initialTime)
// Create a token
token := createToken(h.Ctx(), h.Client())
// Advance time by 2 hours
h.SetTime(initialTime.Add(2 * time.Hour))
// Token should now be expired
err := validateToken(h.Ctx(), h.Client(), token)
Expect(err).To(MatchError("token expired"))
})
Test Harness Details
Database Management
The harness creates a unique database for each test suite:
// Database name format
dbName := "keyline_test_" + uuid.New().String() // e.g., keyline_test_550e8400e29b41d4a716446655440000
// Automatic cleanup
// Database is dropped when h.Close() is called
Server Configuration
Each test suite gets its own server:
// Port allocation (avoids conflicts)
port := 25001 // Incremented for each new harness
// Server URL
externalUrl := fmt.Sprintf("http://localhost:%d", port)
Initial Test Data
The harness automatically creates:
-
Virtual Server: Named "test-vs"
- Signing algorithm: EdDSA
- Registration enabled
-
Admin User: For authenticated operations
- Username:
test-admin-user - Email:
test-admin-user@localhost - Password: Pre-hashed (matches config) WIP
- Username:
-
Admin UI Application: System application for admin operations
Best Practices
1. Use Ordered Tests
// ✓ Good: Dependent tests, since each Describe blocks shares one DB and application instance
var _ = Describe("Complete Flow", Ordered, func() {
It("step 1", func() { /* ... */ })
It("step 2", func() { /* depends on step 1 */ })
})
2. Clean Up Resources
Clean up resources that are not created by the keyline server. The test harness cleans up db and the server itself automatically.
3. Use Descriptive Test Names
// ✓ Good: Clear and descriptive
It("creates a public application without a client secret", func() { /* ... */ })
It("rejects requests with invalid redirect URIs", func() { /* ... */ })
// ✗ Avoid: Vague descriptions
It("works", func() { /* ... */ })
It("test 1", func() { /* ... */ })
4. Test Both Happy and Error Paths
Describe("Application Creation", func() {
It("succeeds with valid data", func() { /* happy path */ })
It("fails without authentication", func() { /* error path */ })
It("fails with invalid type", func() { /* error path */ })
It("fails with malformed redirect URI", func() { /* error path */ })
})
5. Use Gomega Matchers Effectively
// ✓ Good: Descriptive matchers
Expect(err).ToNot(HaveOccurred())
Expect(app.Id).ToNot(BeEmpty())
Expect(app.Type).To(Equal("public"))
Expect(apps.Items).To(HaveLen(3))
Expect(err.Error()).To(ContainSubstring("401 Unauthorized"))
// ⚠ Less Descriptive
Expect(err == nil).To(BeTrue())
Expect(app.Id.String() != "").To(BeTrue())
6. Structure Complex Tests
It("handles complex workflow", func() {
// Arrange
app := createTestApplication(h)
user := createTestUser(h)
// Act
result := performComplexOperation(h, app, user)
// Assert
Expect(result.Success).To(BeTrue())
Expect(result.Data).ToNot(BeNil())
})
CI/CD Integration
E2E tests are included in the CI pipeline and run after the integration tests have passed.
Related Documentation
- API Client Documentation - Learn about the Keyline API client
- Main README - Project overview and setup
- Ginkgo Documentation - Testing framework
- Gomega Documentation - Matcher library
File Structure
tests/e2e/
├── README.md # This file
├── suite_test.go # Test suite entry point
├── harness.go # Test harness implementation
├── application_flow_test.go # Application-related E2E tests
└── ... (additional test files)
Contributing
When adding new E2E tests:
- Create a new file with
*_test.gosuffix - Add
//go:build e2ebuild tag at the top - Use the test harness for setup
- Follow the BDD style with Ginkgo/Gomega
- Include both happy and error paths
- Add descriptive test names
- Clean up resources in
AfterAllor within tests - Update this README if adding new patterns or utilities
For questions or issues, please refer to the main project documentation or open an issue on GitHub.
Documentation
¶
There is no documentation for this package.