added some tests

This commit is contained in:
anonpenguin23 2026-01-22 14:39:50 +02:00
parent 9fc9bbb8e5
commit 0dcde29f7c
51 changed files with 6586 additions and 102 deletions

View File

@ -82,7 +82,7 @@ jobs:
Priority: optional
Architecture: ${ARCH}
Depends: libc6
Maintainer: DeBros Team <team@debros.network>
Maintainer: DeBros Team <team@orama.network>
Description: Orama Network - Distributed P2P Database System
Orama is a distributed peer-to-peer network that combines
RQLite for distributed SQL, IPFS for content-addressed storage,

760
docs/TESTING_PLAN.md Normal file
View File

@ -0,0 +1,760 @@
# Comprehensive Testing Plan
This document outlines the complete testing strategy for the namespace isolation and custom deployment system.
## Table of Contents
1. [Unit Tests](#unit-tests)
2. [Integration Tests](#integration-tests)
3. [End-to-End Tests](#end-to-end-tests)
4. [CLI Tests](#cli-tests)
5. [Performance Tests](#performance-tests)
6. [Security Tests](#security-tests)
7. [Chaos/Failure Tests](#chaos-failure-tests)
---
## 1. Unit Tests
### 1.1 Port Allocator Tests
**File**: `pkg/deployments/port_allocator_test.go`
**Test Cases**:
- ✅ Allocate first port (should be 10100)
- ✅ Allocate sequential ports
- ✅ Find gaps in allocation
- ✅ Handle port exhaustion (all 10000 ports used)
- ✅ Concurrent allocation with race detector
- ✅ Conflict retry with exponential backoff
**Command**:
```bash
go test ./pkg/deployments -run TestPortAllocator -v
```
### 1.2 Home Node Manager Tests
**File**: `pkg/deployments/home_node_test.go`
**Test Cases**:
- ✅ Assign namespace to node with lowest load
- ✅ Reuse existing home node for namespace
- ✅ Weight calculation (deployments, ports, memory, CPU)
- ✅ Handle no nodes available
- ✅ Node failure detection and reassignment
**Command**:
```bash
go test ./pkg/deployments -run TestHomeNodeManager -v
```
### 1.3 Health Checker Tests
**File**: `pkg/deployments/health/checker_test.go` (needs to be created)
**Test Cases**:
- Check static deployment (always healthy)
- Check dynamic deployment with health endpoint
- Mark as failed after 3 consecutive failures
- Record health check history
- Parallel health checking
**Command**:
```bash
go test ./pkg/deployments/health -v
```
### 1.4 Process Manager Tests
**File**: `pkg/deployments/process/manager_test.go` (needs to be created)
**Test Cases**:
- Create systemd service file
- Start deployment process
- Stop deployment process
- Restart deployment
- Read logs from journalctl
**Command**:
```bash
go test ./pkg/deployments/process -v
```
### 1.5 Deployment Service Tests
**File**: `pkg/gateway/handlers/deployments/service_test.go` (needs to be created)
**Test Cases**:
- Create deployment
- Get deployment by ID
- List deployments for namespace
- Update deployment
- Delete deployment
- Record deployment history
**Command**:
```bash
go test ./pkg/gateway/handlers/deployments -v
```
---
## 2. Integration Tests
### 2.1 Static Deployment Integration Test
**File**: `tests/integration/static_deployment_test.go` (needs to be created)
**Setup**:
- Start test RQLite instance
- Start test IPFS node
- Start test gateway
**Test Flow**:
1. Upload static content tarball
2. Verify deployment created in database
3. Verify content uploaded to IPFS
4. Verify DNS record created
5. Test HTTP request to deployment domain
6. Verify content served correctly
**Command**:
```bash
go test ./tests/integration -run TestStaticDeployment -v
```
### 2.2 Next.js SSR Deployment Integration Test
**File**: `tests/integration/nextjs_deployment_test.go` (needs to be created)
**Test Flow**:
1. Upload Next.js build
2. Verify systemd service created
3. Verify process started
4. Wait for health check to pass
5. Test HTTP request to deployment
6. Verify SSR response
**Command**:
```bash
go test ./tests/integration -run TestNextJSDeployment -v
```
### 2.3 SQLite Database Integration Test
**File**: `tests/integration/sqlite_test.go` (needs to be created)
**Test Flow**:
1. Create SQLite database
2. Verify database file created on disk
3. Execute CREATE TABLE query
4. Execute INSERT query
5. Execute SELECT query
6. Backup database to IPFS
7. Verify backup CID recorded
**Command**:
```bash
go test ./tests/integration -run TestSQLiteDatabase -v
```
### 2.4 Custom Domain Integration Test
**File**: `tests/integration/custom_domain_test.go` (needs to be created)
**Test Flow**:
1. Add custom domain to deployment
2. Verify TXT record verification token generated
3. Mock DNS TXT record lookup
4. Verify domain
5. Verify DNS A record created
6. Test HTTP request to custom domain
**Command**:
```bash
go test ./tests/integration -run TestCustomDomain -v
```
### 2.5 Update and Rollback Integration Test
**File**: `tests/integration/update_rollback_test.go` (needs to be created)
**Test Flow**:
1. Deploy initial version
2. Update deployment with new content
3. Verify version incremented
4. Verify new content served
5. Rollback to previous version
6. Verify old content served
7. Verify history recorded correctly
**Command**:
```bash
go test ./tests/integration -run TestUpdateRollback -v
```
---
## 3. End-to-End Tests
### 3.1 Full Static Deployment E2E Test
**File**: `tests/e2e/static_deployment_test.go` (needs to be created)
**Prerequisites**:
- Running RQLite cluster
- Running IPFS cluster
- Running gateway instances
- CoreDNS configured
**Test Flow**:
1. Create test React app
2. Build app (`npm run build`)
3. Deploy via CLI: `orama deploy static ./dist --name e2e-static`
4. Wait for deployment to be active
5. Resolve deployment domain via DNS
6. Make HTTPS request to deployment
7. Verify all static assets load correctly
8. Test SPA routing (fallback to index.html)
9. Update deployment
10. Verify zero-downtime update
11. Delete deployment
12. Verify cleanup
**Command**:
```bash
go test ./tests/e2e -run TestStaticDeploymentE2E -v -timeout 10m
```
### 3.2 Full Next.js SSR Deployment E2E Test
**File**: `tests/e2e/nextjs_deployment_test.go` (needs to be created)
**Test Flow**:
1. Create test Next.js app with API routes
2. Build app (`npm run build`)
3. Deploy via CLI: `orama deploy nextjs . --name e2e-nextjs --ssr`
4. Wait for process to start and pass health check
5. Test static route
6. Test API route
7. Test SSR page
8. Update deployment with graceful restart
9. Verify health check before cutting over
10. Rollback if health check fails
11. Monitor logs during deployment
12. Delete deployment
**Command**:
```bash
go test ./tests/e2e -run TestNextJSDeploymentE2E -v -timeout 15m
```
### 3.3 Full SQLite Database E2E Test
**File**: `tests/e2e/sqlite_test.go` (needs to be created)
**Test Flow**:
1. Create database via CLI: `orama db create e2e-testdb`
2. Create schema: `orama db query e2e-testdb "CREATE TABLE ..."`
3. Insert data: `orama db query e2e-testdb "INSERT ..."`
4. Query data: `orama db query e2e-testdb "SELECT ..."`
5. Verify results match expected
6. Backup database: `orama db backup e2e-testdb`
7. List backups: `orama db backups e2e-testdb`
8. Verify backup CID in IPFS
9. Restore from backup (if implemented)
10. Verify data integrity
**Command**:
```bash
go test ./tests/e2e -run TestSQLiteDatabaseE2E -v -timeout 10m
```
### 3.4 DNS Resolution E2E Test
**File**: `tests/e2e/dns_test.go` (needs to be created)
**Test Flow**:
1. Create deployment
2. Query all 4 nameservers for deployment domain
3. Verify all return same IP
4. Add custom domain
5. Verify TXT record
6. Verify A record created
7. Query external DNS resolver
8. Verify domain resolves correctly
**Command**:
```bash
go test ./tests/e2e -run TestDNSResolutionE2E -v -timeout 5m
```
---
## 4. CLI Tests
### 4.1 Deploy Command Tests
**File**: `tests/cli/deploy_test.go` (needs to be created)
**Test Cases**:
- Deploy static site
- Deploy Next.js with --ssr flag
- Deploy Node.js backend
- Deploy Go backend
- Handle missing arguments
- Handle invalid paths
- Handle network errors gracefully
**Command**:
```bash
go test ./tests/cli -run TestDeployCommand -v
```
### 4.2 Deployments Management Tests
**File**: `tests/cli/deployments_test.go` (needs to be created)
**Test Cases**:
- List all deployments
- Get specific deployment
- Delete deployment with confirmation
- Rollback to version
- View logs with --follow
- Filter deployments by status
**Command**:
```bash
go test ./tests/cli -run TestDeploymentsCommands -v
```
### 4.3 Database Command Tests
**File**: `tests/cli/db_test.go` (needs to be created)
**Test Cases**:
- Create database
- Execute query (SELECT, INSERT, UPDATE, DELETE)
- List databases
- Backup database
- List backups
- Handle SQL syntax errors
- Handle connection errors
**Command**:
```bash
go test ./tests/cli -run TestDatabaseCommands -v
```
### 4.4 Domain Command Tests
**File**: `tests/cli/domain_test.go` (needs to be created)
**Test Cases**:
- Add custom domain
- Verify domain
- List domains
- Remove domain
- Handle verification failures
**Command**:
```bash
go test ./tests/cli -run TestDomainCommands -v
```
---
## 5. Performance Tests
### 5.1 Concurrent Deployment Test
**Objective**: Verify system handles multiple concurrent deployments
**Test**:
```bash
# Deploy 50 static sites concurrently
for i in {1..50}; do
orama deploy static ./test-site --name test-$i &
done
wait
# Verify all succeeded
orama deployments list | grep -c "active"
# Should output: 50
```
### 5.2 Port Allocation Performance Test
**Objective**: Measure port allocation speed under high contention
**Test**:
```go
func BenchmarkPortAllocation(b *testing.B) {
// Setup
db := setupTestDB()
allocator := deployments.NewPortAllocator(db, logger)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, err := allocator.AllocatePort(ctx, "test-node", uuid.New().String())
if err != nil {
b.Fatal(err)
}
}
})
}
```
**Command**:
```bash
go test -bench=BenchmarkPortAllocation -benchtime=10s ./pkg/deployments
```
### 5.3 DNS Query Performance Test
**Objective**: Measure CoreDNS query latency with RQLite backend
**Test**:
```bash
# Warm up
for i in {1..1000}; do
dig @localhost test.orama.network > /dev/null
done
# Benchmark
ab -n 10000 -c 100 http://localhost:53/dns-query?name=test.orama.network
# Expected: <50ms p95 latency
```
### 5.4 Health Check Performance Test
**Objective**: Verify health checker handles 1000 deployments
**Test**:
- Create 1000 test deployments
- Start health checker
- Measure time to complete one check cycle
- Expected: <60 seconds for all 1000 checks
---
## 6. Security Tests
### 6.1 Namespace Isolation Test
**Objective**: Verify users cannot access other namespaces' resources
**Test**:
```bash
# User A deploys
export ORAMA_TOKEN="user-a-token"
orama deploy static ./site --name myapp
# User B attempts to access User A's deployment
export ORAMA_TOKEN="user-b-token"
orama deployments get myapp
# Expected: 404 Not Found or 403 Forbidden
# User B attempts to access User A's database
orama db query user-a-db "SELECT * FROM users"
# Expected: 404 Not Found or 403 Forbidden
```
### 6.2 SQL Injection Test
**Objective**: Verify SQLite handler sanitizes inputs
**Test**:
```bash
# Attempt SQL injection in database name
orama db create "test'; DROP TABLE users; --"
# Expected: Validation error
# Attempt SQL injection in query
orama db query testdb "SELECT * FROM users WHERE id = '1' OR '1'='1'"
# Expected: Query executes safely (parameterized)
```
### 6.3 Path Traversal Test
**Objective**: Verify deployment paths are sanitized
**Test**:
```bash
# Attempt path traversal in deployment name
orama deploy static ./site --name "../../etc/passwd"
# Expected: Validation error
# Attempt path traversal in SQLite database name
orama db create "../../../etc/shadow"
# Expected: Validation error
```
### 6.4 Resource Exhaustion Test
**Objective**: Verify resource limits are enforced
**Test**:
```bash
# Deploy 10001 sites (exceeds default limit)
for i in {1..10001}; do
orama deploy static ./site --name test-$i
done
# Expected: Last deployment rejected with quota error
# Create huge SQLite database
orama db query bigdb "CREATE TABLE huge (data TEXT)"
orama db query bigdb "INSERT INTO huge VALUES ('$(head -c 10G </dev/urandom | base64)')"
# Expected: Size limit enforced
```
---
## 7. Chaos/Failure Tests
### 7.1 Node Failure Test
**Objective**: Verify system handles gateway node failure
**Test**:
1. Deploy app to node A
2. Verify deployment is healthy
3. Simulate node A crash: `systemctl stop orama-gateway`
4. Wait for failure detection
5. Verify namespace migrated to node B
6. Verify deployment restored from IPFS backup
7. Verify health checks pass on node B
### 7.2 RQLite Failure Test
**Objective**: Verify graceful degradation when RQLite is unavailable
**Test**:
1. Deploy app successfully
2. Stop RQLite: `systemctl stop rqlite`
3. Attempt new deployment (should fail gracefully with error message)
4. Verify existing deployments still serve traffic (cached)
5. Restart RQLite
6. Verify new deployments work
### 7.3 IPFS Failure Test
**Objective**: Verify handling of IPFS unavailability
**Test**:
1. Deploy static site
2. Stop IPFS: `systemctl stop ipfs`
3. Attempt to serve deployment (should fail or serve from cache)
4. Attempt new deployment (should fail with clear error)
5. Restart IPFS
6. Verify recovery
### 7.4 CoreDNS Failure Test
**Objective**: Verify DNS redundancy
**Test**:
1. Stop 1 CoreDNS instance
2. Verify DNS still resolves (3 of 4 servers working)
3. Stop 2nd CoreDNS instance
4. Verify DNS still resolves (2 of 4 servers working)
5. Stop 3rd CoreDNS instance
6. Verify DNS degraded but functional (1 of 4 servers)
7. Stop all 4 CoreDNS instances
8. Verify DNS resolution fails
### 7.5 Concurrent Update Test
**Objective**: Verify race conditions are handled
**Test**:
```bash
# Update same deployment concurrently
orama deploy static ./site-v2 --name myapp --update &
orama deploy static ./site-v3 --name myapp --update &
wait
# Verify only one update succeeded
# Verify database is consistent
# Verify no partial updates
```
---
## Test Execution Plan
### Phase 1: Unit Tests (Day 1)
- Run all unit tests
- Ensure 100% pass rate
- Measure code coverage (target: >80%)
### Phase 2: Integration Tests (Days 2-3)
- Run integration tests in isolated environment
- Fix any integration issues
- Verify database state consistency
### Phase 3: E2E Tests (Days 4-5)
- Run E2E tests in staging environment
- Test with real DNS, IPFS, RQLite
- Fix any environment-specific issues
### Phase 4: Performance Tests (Day 6)
- Run load tests
- Measure latency and throughput
- Optimize bottlenecks
### Phase 5: Security Tests (Day 7)
- Run security test suite
- Fix any vulnerabilities
- Document security model
### Phase 6: Chaos Tests (Day 8)
- Run failure scenario tests
- Verify recovery procedures
- Document failure modes
### Phase 7: Production Validation (Day 9-10)
- Deploy to production with feature flag OFF
- Run smoke tests in production
- Enable feature flag for 10% of traffic
- Monitor for 24 hours
- Gradually increase to 100%
---
## Test Environment Setup
### Local Development
```bash
# Start test dependencies
docker-compose -f tests/docker-compose.test.yml up -d
# Run unit tests
make test-unit
# Run integration tests
make test-integration
# Run all tests
make test-all
```
### CI/CD Pipeline
```yaml
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
- run: go test ./pkg/... -v -race -coverprofile=coverage.out
integration-tests:
runs-on: ubuntu-latest
services:
rqlite:
image: rqlite/rqlite
ipfs:
image: ipfs/go-ipfs
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
- run: go test ./tests/integration/... -v -timeout 15m
e2e-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: ./scripts/setup-test-env.sh
- run: go test ./tests/e2e/... -v -timeout 30m
```
---
## Success Criteria
### Unit Tests
- ✅ All tests pass
- ✅ Code coverage >80%
- ✅ No race conditions detected
### Integration Tests
- ✅ All happy path scenarios pass
- ✅ Error scenarios handled gracefully
- ✅ Database state remains consistent
### E2E Tests
- ✅ Full workflows complete successfully
- ✅ DNS resolution works across all nameservers
- ✅ Deployments accessible via HTTPS
### Performance Tests
- ✅ Port allocation: <10ms per allocation
- ✅ DNS queries: <50ms p95 latency
- ✅ Deployment creation: <30s for static, <2min for dynamic
- ✅ Health checks: Complete 1000 deployments in <60s
### Security Tests
- ✅ Namespace isolation enforced
- ✅ No SQL injection vulnerabilities
- ✅ No path traversal vulnerabilities
- ✅ Resource limits enforced
### Chaos Tests
- ✅ Node failure: Recovery within 5 minutes
- ✅ Service failure: Graceful degradation
- ✅ Concurrent updates: No race conditions
---
## Ongoing Testing
After production deployment:
1. **Synthetic Monitoring**: Create test deployments every hour and verify they work
2. **Canary Deployments**: Test new versions with 1% of traffic before full rollout
3. **Load Testing**: Weekly load tests to ensure performance doesn't degrade
4. **Security Scanning**: Automated vulnerability scans
5. **Chaos Engineering**: Monthly chaos tests in staging
---
## Test Automation Commands
```bash
# Run all tests
make test-all
# Run unit tests only
make test-unit
# Run integration tests only
make test-integration
# Run E2E tests only
make test-e2e
# Run performance tests
make test-performance
# Run security tests
make test-security
# Run chaos tests
make test-chaos
# Generate coverage report
make test-coverage
# Run tests with race detector
make test-race
```

View File

@ -0,0 +1,194 @@
//go:build e2e
package deployments_test
import (
"fmt"
"io"
"net/http"
"path/filepath"
"testing"
"time"
"github.com/DeBrosOfficial/network/e2e"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestStaticDeployment_FullFlow(t *testing.T) {
env, err := e2e.LoadTestEnv()
require.NoError(t, err, "Failed to load test environment")
deploymentName := fmt.Sprintf("test-static-%d", time.Now().Unix())
tarballPath := filepath.Join("../../testdata/tarballs/react-vite.tar.gz")
var deploymentID string
// Cleanup after test
defer func() {
if !env.SkipCleanup && deploymentID != "" {
e2e.DeleteDeployment(t, env, deploymentID)
}
}()
t.Run("Upload static tarball", func(t *testing.T) {
deploymentID = e2e.CreateTestDeployment(t, env, deploymentName, tarballPath)
assert.NotEmpty(t, deploymentID, "Deployment ID should not be empty")
t.Logf("✓ Created deployment: %s (ID: %s)", deploymentName, deploymentID)
})
t.Run("Verify deployment in database", func(t *testing.T) {
deployment := e2e.GetDeployment(t, env, deploymentID)
assert.Equal(t, deploymentName, deployment["name"], "Deployment name should match")
assert.NotEmpty(t, deployment["content_cid"], "Content CID should not be empty")
// Status might be "deploying" or "active" depending on timing
status, ok := deployment["status"].(string)
require.True(t, ok, "Status should be a string")
assert.Contains(t, []string{"deploying", "active"}, status, "Status should be deploying or active")
t.Logf("✓ Deployment verified in database")
t.Logf(" - Name: %s", deployment["name"])
t.Logf(" - Status: %s", status)
t.Logf(" - CID: %s", deployment["content_cid"])
})
t.Run("Verify DNS record creation", func(t *testing.T) {
// Wait for deployment to become active
time.Sleep(2 * time.Second)
// Expected domain format: {deploymentName}.orama.network
expectedDomain := fmt.Sprintf("%s.orama.network", deploymentName)
// Make request with Host header (localhost testing)
resp := e2e.TestDeploymentWithHostHeader(t, env, expectedDomain, "/")
defer resp.Body.Close()
// Should return 200 with React app HTML
assert.Equal(t, http.StatusOK, resp.StatusCode, "Should return 200 OK")
body, err := io.ReadAll(resp.Body)
require.NoError(t, err, "Should read response body")
bodyStr := string(body)
// Verify React app content
assert.Contains(t, bodyStr, "<div id=\"root\">", "Should contain React root div")
assert.Contains(t, resp.Header.Get("Content-Type"), "text/html", "Content-Type should be text/html")
t.Logf("✓ Domain routing works")
t.Logf(" - Domain: %s", expectedDomain)
t.Logf(" - Status: %d", resp.StatusCode)
t.Logf(" - Content-Type: %s", resp.Header.Get("Content-Type"))
})
t.Run("Verify static assets serve correctly", func(t *testing.T) {
expectedDomain := fmt.Sprintf("%s.orama.network", deploymentName)
// Test CSS file (exact path depends on Vite build output)
// We'll just test a few common asset paths
assetPaths := []struct {
path string
contentType string
}{
{"/index.html", "text/html"},
// Note: Asset paths with hashes change on each build
// We'll test what we can
}
for _, asset := range assetPaths {
resp := e2e.TestDeploymentWithHostHeader(t, env, expectedDomain, asset.path)
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
assert.Contains(t, resp.Header.Get("Content-Type"), asset.contentType,
"Content-Type should be %s for %s", asset.contentType, asset.path)
t.Logf("✓ Asset served correctly: %s (%s)", asset.path, asset.contentType)
}
}
})
t.Run("Verify SPA fallback routing", func(t *testing.T) {
expectedDomain := fmt.Sprintf("%s.orama.network", deploymentName)
// Request unknown route (should return index.html for SPA)
resp := e2e.TestDeploymentWithHostHeader(t, env, expectedDomain, "/about/team")
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode, "SPA fallback should return 200")
body, err := io.ReadAll(resp.Body)
require.NoError(t, err, "Should read response body")
assert.Contains(t, string(body), "<div id=\"root\">", "Should return index.html for unknown paths")
t.Logf("✓ SPA fallback routing works")
})
t.Run("List deployments", func(t *testing.T) {
req, err := http.NewRequest("GET", env.GatewayURL+"/v1/deployments/list", nil)
require.NoError(t, err, "Should create request")
req.Header.Set("Authorization", "Bearer "+env.APIKey)
resp, err := env.HTTPClient.Do(req)
require.NoError(t, err, "Should execute request")
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode, "List deployments should return 200")
var result map[string]interface{}
require.NoError(t, e2e.DecodeJSON(mustReadAll(t, resp.Body), &result), "Should decode JSON")
deployments, ok := result["deployments"].([]interface{})
require.True(t, ok, "Deployments should be an array")
assert.GreaterOrEqual(t, len(deployments), 1, "Should have at least one deployment")
// Find our deployment
found := false
for _, d := range deployments {
dep, ok := d.(map[string]interface{})
if !ok {
continue
}
if dep["name"] == deploymentName {
found = true
t.Logf("✓ Found deployment in list: %s", deploymentName)
break
}
}
assert.True(t, found, "Deployment should be in list")
})
t.Run("Delete deployment", func(t *testing.T) {
e2e.DeleteDeployment(t, env, deploymentID)
// Verify deletion
time.Sleep(1 * time.Second)
req, _ := http.NewRequest("GET", env.GatewayURL+"/v1/deployments/get?id="+deploymentID, nil)
req.Header.Set("Authorization", "Bearer "+env.APIKey)
resp, err := env.HTTPClient.Do(req)
require.NoError(t, err, "Should execute request")
defer resp.Body.Close()
assert.Equal(t, http.StatusNotFound, resp.StatusCode, "Deleted deployment should return 404")
t.Logf("✓ Deployment deleted successfully")
// Clear deploymentID so cleanup doesn't try to delete again
deploymentID = ""
})
}
func mustReadAll(t *testing.T, r io.Reader) []byte {
t.Helper()
data, err := io.ReadAll(r)
require.NoError(t, err, "Should read all data")
return data
}

257
e2e/domain_routing_test.go Normal file
View File

@ -0,0 +1,257 @@
//go:build e2e
package e2e
import (
"encoding/json"
"fmt"
"io"
"net/http"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDomainRouting_BasicRouting(t *testing.T) {
env, err := LoadTestEnv()
require.NoError(t, err, "Failed to load test environment")
deploymentName := fmt.Sprintf("test-routing-%d", time.Now().Unix())
tarballPath := filepath.Join("../testdata/tarballs/react-vite.tar.gz")
deploymentID := CreateTestDeployment(t, env, deploymentName, tarballPath)
defer func() {
if !env.SkipCleanup {
DeleteDeployment(t, env, deploymentID)
}
}()
// Wait for deployment to be active
time.Sleep(2 * time.Second)
t.Run("Standard domain resolves", func(t *testing.T) {
// Domain format: {deploymentName}.orama.network
domain := fmt.Sprintf("%s.orama.network", deploymentName)
resp := TestDeploymentWithHostHeader(t, env, domain, "/")
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode, "Should return 200 OK")
body, err := io.ReadAll(resp.Body)
require.NoError(t, err, "Should read response body")
assert.Contains(t, string(body), "<div id=\"root\">", "Should serve React app")
assert.Contains(t, resp.Header.Get("Content-Type"), "text/html", "Content-Type should be HTML")
t.Logf("✓ Standard domain routing works: %s", domain)
})
t.Run("Non-debros domain passes through", func(t *testing.T) {
// Request with non-debros domain should not route to deployment
resp := TestDeploymentWithHostHeader(t, env, "example.com", "/")
defer resp.Body.Close()
// Should either return 404 or pass to default handler
assert.NotEqual(t, http.StatusOK, resp.StatusCode,
"Non-debros domain should not route to deployment")
t.Logf("✓ Non-debros domains correctly pass through (status: %d)", resp.StatusCode)
})
t.Run("API paths bypass domain routing", func(t *testing.T) {
// /v1/* paths should bypass domain routing and use API key auth
domain := fmt.Sprintf("%s.orama.network", deploymentName)
req, _ := http.NewRequest("GET", env.GatewayURL+"/v1/deployments/list", nil)
req.Host = domain
req.Header.Set("Authorization", "Bearer "+env.APIKey)
resp, err := env.HTTPClient.Do(req)
require.NoError(t, err, "Should execute request")
defer resp.Body.Close()
// Should return API response, not deployment content
assert.Equal(t, http.StatusOK, resp.StatusCode, "API endpoint should work")
var result map[string]interface{}
bodyBytes, _ := io.ReadAll(resp.Body)
err = json.Unmarshal(bodyBytes, &result)
// Should be JSON API response
assert.NoError(t, err, "Should decode JSON (API response)")
assert.NotNil(t, result["deployments"], "Should have deployments field")
t.Logf("✓ API paths correctly bypass domain routing")
})
t.Run("Well-known paths bypass domain routing", func(t *testing.T) {
domain := fmt.Sprintf("%s.orama.network", deploymentName)
// /.well-known/ paths should bypass (used for ACME challenges, etc.)
resp := TestDeploymentWithHostHeader(t, env, domain, "/.well-known/acme-challenge/test")
defer resp.Body.Close()
// Should not serve deployment content
// Exact status depends on implementation, but shouldn't be deployment content
body, _ := io.ReadAll(resp.Body)
bodyStr := string(body)
// Shouldn't contain React app content
if resp.StatusCode == http.StatusOK {
assert.NotContains(t, bodyStr, "<div id=\"root\">",
"Well-known paths should not serve deployment content")
}
t.Logf("✓ Well-known paths bypass routing (status: %d)", resp.StatusCode)
})
}
func TestDomainRouting_MultipleDeployments(t *testing.T) {
env, err := LoadTestEnv()
require.NoError(t, err, "Failed to load test environment")
tarballPath := filepath.Join("../testdata/tarballs/react-vite.tar.gz")
// Create multiple deployments
deployment1Name := fmt.Sprintf("test-multi-1-%d", time.Now().Unix())
deployment2Name := fmt.Sprintf("test-multi-2-%d", time.Now().Unix())
deployment1ID := CreateTestDeployment(t, env, deployment1Name, tarballPath)
time.Sleep(1 * time.Second)
deployment2ID := CreateTestDeployment(t, env, deployment2Name, tarballPath)
defer func() {
if !env.SkipCleanup {
DeleteDeployment(t, env, deployment1ID)
DeleteDeployment(t, env, deployment2ID)
}
}()
time.Sleep(2 * time.Second)
t.Run("Each deployment routes independently", func(t *testing.T) {
domain1 := fmt.Sprintf("%s.orama.network", deployment1Name)
domain2 := fmt.Sprintf("%s.orama.network", deployment2Name)
// Test deployment 1
resp1 := TestDeploymentWithHostHeader(t, env, domain1, "/")
defer resp1.Body.Close()
assert.Equal(t, http.StatusOK, resp1.StatusCode, "Deployment 1 should serve")
// Test deployment 2
resp2 := TestDeploymentWithHostHeader(t, env, domain2, "/")
defer resp2.Close()
assert.Equal(t, http.StatusOK, resp2.StatusCode, "Deployment 2 should serve")
t.Logf("✓ Multiple deployments route independently")
t.Logf(" - Domain 1: %s", domain1)
t.Logf(" - Domain 2: %s", domain2)
})
t.Run("Wrong domain returns 404", func(t *testing.T) {
// Request with non-existent deployment subdomain
fakeDeploymentDomain := fmt.Sprintf("nonexistent-deployment-%d.orama.network", time.Now().Unix())
resp := TestDeploymentWithHostHeader(t, env, fakeDeploymentDomain, "/")
defer resp.Body.Close()
assert.Equal(t, http.StatusNotFound, resp.StatusCode,
"Non-existent deployment should return 404")
t.Logf("✓ Non-existent deployment returns 404")
})
}
func TestDomainRouting_ContentTypes(t *testing.T) {
env, err := LoadTestEnv()
require.NoError(t, err, "Failed to load test environment")
deploymentName := fmt.Sprintf("test-content-types-%d", time.Now().Unix())
tarballPath := filepath.Join("../testdata/tarballs/react-vite.tar.gz")
deploymentID := CreateTestDeployment(t, env, deploymentName, tarballPath)
defer func() {
if !env.SkipCleanup {
DeleteDeployment(t, env, deploymentID)
}
}()
time.Sleep(2 * time.Second)
domain := fmt.Sprintf("%s.orama.network", deploymentName)
contentTypeTests := []struct {
path string
shouldHave string
description string
}{
{"/", "text/html", "HTML root"},
{"/index.html", "text/html", "HTML file"},
}
for _, test := range contentTypeTests {
t.Run(test.description, func(t *testing.T) {
resp := TestDeploymentWithHostHeader(t, env, domain, test.path)
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
contentType := resp.Header.Get("Content-Type")
assert.Contains(t, contentType, test.shouldHave,
"Content-Type for %s should contain %s", test.path, test.shouldHave)
t.Logf("✓ %s: %s", test.description, contentType)
} else {
t.Logf("⚠ %s returned status %d", test.path, resp.StatusCode)
}
})
}
}
func TestDomainRouting_SPAFallback(t *testing.T) {
env, err := LoadTestEnv()
require.NoError(t, err, "Failed to load test environment")
deploymentName := fmt.Sprintf("test-spa-%d", time.Now().Unix())
tarballPath := filepath.Join("../testdata/tarballs/react-vite.tar.gz")
deploymentID := CreateTestDeployment(t, env, deploymentName, tarballPath)
defer func() {
if !env.SkipCleanup {
DeleteDeployment(t, env, deploymentID)
}
}()
time.Sleep(2 * time.Second)
domain := fmt.Sprintf("%s.orama.network", deploymentName)
t.Run("Unknown paths fall back to index.html", func(t *testing.T) {
unknownPaths := []string{
"/about",
"/users/123",
"/settings/profile",
"/some/deep/nested/path",
}
for _, path := range unknownPaths {
resp := TestDeploymentWithHostHeader(t, env, domain, path)
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
// Should return index.html for SPA routing
assert.Equal(t, http.StatusOK, resp.StatusCode,
"SPA fallback should return 200 for %s", path)
assert.Contains(t, string(body), "<div id=\"root\">",
"SPA fallback should return index.html for %s", path)
}
t.Logf("✓ SPA fallback routing verified for %d paths", len(unknownPaths))
})
}

View File

@ -966,3 +966,371 @@ func (p *WSPubSubClientPair) Close() {
p.Subscriber.Close()
}
}
// ============================================================================
// Deployment Testing Helpers
// ============================================================================
// E2ETestEnv holds the environment configuration for deployment E2E tests
type E2ETestEnv struct {
GatewayURL string
APIKey string
Namespace string
HTTPClient *http.Client
SkipCleanup bool
}
// LoadTestEnv loads the test environment from environment variables
func LoadTestEnv() (*E2ETestEnv, error) {
gatewayURL := os.Getenv("ORAMA_GATEWAY_URL")
if gatewayURL == "" {
gatewayURL = GetGatewayURL()
}
apiKey := os.Getenv("ORAMA_API_KEY")
if apiKey == "" {
apiKey = GetAPIKey()
}
namespace := os.Getenv("ORAMA_NAMESPACE")
if namespace == "" {
namespace = GetClientNamespace()
}
skipCleanup := os.Getenv("ORAMA_SKIP_CLEANUP") == "true"
return &E2ETestEnv{
GatewayURL: gatewayURL,
APIKey: apiKey,
Namespace: namespace,
HTTPClient: NewHTTPClient(30 * time.Second),
SkipCleanup: skipCleanup,
}, nil
}
// LoadTestEnvWithNamespace loads test environment with a specific namespace
func LoadTestEnvWithNamespace(namespace string) (*E2ETestEnv, error) {
env, err := LoadTestEnv()
if err != nil {
return nil, err
}
env.Namespace = namespace
return env, nil
}
// CreateTestDeployment creates a test deployment and returns its ID
func CreateTestDeployment(t *testing.T, env *E2ETestEnv, name, tarballPath string) string {
t.Helper()
file, err := os.Open(tarballPath)
if err != nil {
t.Fatalf("failed to open tarball: %v", err)
}
defer file.Close()
// Create multipart form
body := &bytes.Buffer{}
boundary := "----WebKitFormBoundary7MA4YWxkTrZu0gW"
// Write name field
body.WriteString("--" + boundary + "\r\n")
body.WriteString("Content-Disposition: form-data; name=\"name\"\r\n\r\n")
body.WriteString(name + "\r\n")
// Write subdomain field
body.WriteString("--" + boundary + "\r\n")
body.WriteString("Content-Disposition: form-data; name=\"subdomain\"\r\n\r\n")
body.WriteString(name + "\r\n")
// Write tarball file
body.WriteString("--" + boundary + "\r\n")
body.WriteString("Content-Disposition: form-data; name=\"tarball\"; filename=\"app.tar.gz\"\r\n")
body.WriteString("Content-Type: application/gzip\r\n\r\n")
fileData, _ := io.ReadAll(file)
body.Write(fileData)
body.WriteString("\r\n--" + boundary + "--\r\n")
req, err := http.NewRequest("POST", env.GatewayURL+"/v1/deployments/static/upload", body)
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
req.Header.Set("Content-Type", "multipart/form-data; boundary="+boundary)
req.Header.Set("Authorization", "Bearer "+env.APIKey)
resp, err := env.HTTPClient.Do(req)
if err != nil {
t.Fatalf("failed to upload deployment: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
bodyBytes, _ := io.ReadAll(resp.Body)
t.Fatalf("deployment upload failed with status %d: %s", resp.StatusCode, string(bodyBytes))
}
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
return result["id"].(string)
}
// DeleteDeployment deletes a deployment by ID
func DeleteDeployment(t *testing.T, env *E2ETestEnv, deploymentID string) {
t.Helper()
req, _ := http.NewRequest("DELETE", env.GatewayURL+"/v1/deployments/delete?id="+deploymentID, nil)
req.Header.Set("Authorization", "Bearer "+env.APIKey)
resp, err := env.HTTPClient.Do(req)
if err != nil {
t.Logf("warning: failed to delete deployment: %v", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Logf("warning: delete deployment returned status %d", resp.StatusCode)
}
}
// GetDeployment retrieves deployment metadata by ID
func GetDeployment(t *testing.T, env *E2ETestEnv, deploymentID string) map[string]interface{} {
t.Helper()
req, _ := http.NewRequest("GET", env.GatewayURL+"/v1/deployments/get?id="+deploymentID, nil)
req.Header.Set("Authorization", "Bearer "+env.APIKey)
resp, err := env.HTTPClient.Do(req)
if err != nil {
t.Fatalf("failed to get deployment: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
t.Fatalf("get deployment failed with status %d: %s", resp.StatusCode, string(bodyBytes))
}
var deployment map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&deployment); err != nil {
t.Fatalf("failed to decode deployment: %v", err)
}
return deployment
}
// CreateSQLiteDB creates a SQLite database for a namespace
func CreateSQLiteDB(t *testing.T, env *E2ETestEnv, dbName string) {
t.Helper()
reqBody := map[string]string{"database_name": dbName}
bodyBytes, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", env.GatewayURL+"/v1/db/sqlite/create", bytes.NewReader(bodyBytes))
req.Header.Set("Authorization", "Bearer "+env.APIKey)
req.Header.Set("Content-Type", "application/json")
resp, err := env.HTTPClient.Do(req)
if err != nil {
t.Fatalf("failed to create database: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
bodyBytes, _ := io.ReadAll(resp.Body)
t.Fatalf("create database failed with status %d: %s", resp.StatusCode, string(bodyBytes))
}
}
// DeleteSQLiteDB deletes a SQLite database
func DeleteSQLiteDB(t *testing.T, env *E2ETestEnv, dbName string) {
t.Helper()
reqBody := map[string]string{"database_name": dbName}
bodyBytes, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("DELETE", env.GatewayURL+"/v1/db/sqlite/delete", bytes.NewReader(bodyBytes))
req.Header.Set("Authorization", "Bearer "+env.APIKey)
req.Header.Set("Content-Type", "application/json")
resp, err := env.HTTPClient.Do(req)
if err != nil {
t.Logf("warning: failed to delete database: %v", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Logf("warning: delete database returned status %d", resp.StatusCode)
}
}
// ExecuteSQLQuery executes a SQL query on a database
func ExecuteSQLQuery(t *testing.T, env *E2ETestEnv, dbName, query string) map[string]interface{} {
t.Helper()
reqBody := map[string]interface{}{
"database_name": dbName,
"query": query,
}
bodyBytes, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", env.GatewayURL+"/v1/db/sqlite/query", bytes.NewReader(bodyBytes))
req.Header.Set("Authorization", "Bearer "+env.APIKey)
req.Header.Set("Content-Type", "application/json")
resp, err := env.HTTPClient.Do(req)
if err != nil {
t.Fatalf("failed to execute query: %v", err)
}
defer resp.Body.Close()
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
t.Fatalf("failed to decode query response: %v", err)
}
if errMsg, ok := result["error"].(string); ok && errMsg != "" {
t.Fatalf("SQL query failed: %s", errMsg)
}
return result
}
// QuerySQLite executes a SELECT query and returns rows
func QuerySQLite(t *testing.T, env *E2ETestEnv, dbName, query string) []map[string]interface{} {
t.Helper()
result := ExecuteSQLQuery(t, env, dbName, query)
rows, ok := result["rows"].([]interface{})
if !ok {
return []map[string]interface{}{}
}
columns, _ := result["columns"].([]interface{})
var results []map[string]interface{}
for _, row := range rows {
rowData, ok := row.([]interface{})
if !ok {
continue
}
rowMap := make(map[string]interface{})
for i, col := range columns {
if i < len(rowData) {
rowMap[col.(string)] = rowData[i]
}
}
results = append(results, rowMap)
}
return results
}
// UploadTestFile uploads a file to IPFS and returns the CID
func UploadTestFile(t *testing.T, env *E2ETestEnv, filename, content string) string {
t.Helper()
body := &bytes.Buffer{}
boundary := "----WebKitFormBoundary7MA4YWxkTrZu0gW"
body.WriteString("--" + boundary + "\r\n")
body.WriteString(fmt.Sprintf("Content-Disposition: form-data; name=\"file\"; filename=\"%s\"\r\n", filename))
body.WriteString("Content-Type: text/plain\r\n\r\n")
body.WriteString(content)
body.WriteString("\r\n--" + boundary + "--\r\n")
req, _ := http.NewRequest("POST", env.GatewayURL+"/v1/storage/upload", body)
req.Header.Set("Content-Type", "multipart/form-data; boundary="+boundary)
req.Header.Set("Authorization", "Bearer "+env.APIKey)
resp, err := env.HTTPClient.Do(req)
if err != nil {
t.Fatalf("failed to upload file: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
bodyBytes, _ := io.ReadAll(resp.Body)
t.Fatalf("upload file failed with status %d: %s", resp.StatusCode, string(bodyBytes))
}
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
t.Fatalf("failed to decode upload response: %v", err)
}
cid, ok := result["cid"].(string)
if !ok {
t.Fatalf("CID not found in response")
}
return cid
}
// UnpinFile unpins a file from IPFS
func UnpinFile(t *testing.T, env *E2ETestEnv, cid string) {
t.Helper()
reqBody := map[string]string{"cid": cid}
bodyBytes, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", env.GatewayURL+"/v1/storage/unpin", bytes.NewReader(bodyBytes))
req.Header.Set("Authorization", "Bearer "+env.APIKey)
req.Header.Set("Content-Type", "application/json")
resp, err := env.HTTPClient.Do(req)
if err != nil {
t.Logf("warning: failed to unpin file: %v", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Logf("warning: unpin file returned status %d", resp.StatusCode)
}
}
// TestDeploymentWithHostHeader tests a deployment by setting the Host header
func TestDeploymentWithHostHeader(t *testing.T, env *E2ETestEnv, host, path string) *http.Response {
t.Helper()
req, err := http.NewRequest("GET", env.GatewayURL+path, nil)
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
req.Host = host
resp, err := env.HTTPClient.Do(req)
if err != nil {
t.Fatalf("failed to test deployment: %v", err)
}
return resp
}
// WaitForHealthy waits for a deployment to become healthy
func WaitForHealthy(t *testing.T, env *E2ETestEnv, deploymentID string, timeout time.Duration) bool {
t.Helper()
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
deployment := GetDeployment(t, env, deploymentID)
if status, ok := deployment["status"].(string); ok && status == "active" {
return true
}
time.Sleep(1 * time.Second)
}
return false
}

View File

@ -0,0 +1,292 @@
//go:build e2e
package e2e
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestFullStack_GoAPI_SQLite(t *testing.T) {
env, err := LoadTestEnv()
require.NoError(t, err, "Failed to load test environment")
appName := fmt.Sprintf("fullstack-app-%d", time.Now().Unix())
backendName := appName + "-backend"
dbName := appName + "-db"
var backendID string
defer func() {
if !env.SkipCleanup {
if backendID != "" {
DeleteDeployment(t, env, backendID)
}
DeleteSQLiteDB(t, env, dbName)
}
}()
// Step 1: Create SQLite database
t.Run("Create SQLite database", func(t *testing.T) {
CreateSQLiteDB(t, env, dbName)
// Create users table
query := `CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)`
ExecuteSQLQuery(t, env, dbName, query)
// Insert test data
insertQuery := `INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com')`
result := ExecuteSQLQuery(t, env, dbName, insertQuery)
assert.NotNil(t, result, "Should execute INSERT successfully")
t.Logf("✓ Database created with users table")
})
// Step 2: Deploy Go backend (this would normally connect to SQLite)
// Note: For now we test the Go backend deployment without actual DB connection
// as that requires environment variable injection during deployment
t.Run("Deploy Go backend", func(t *testing.T) {
tarballPath := filepath.Join("../testdata/tarballs/go-backend.tar.gz")
// Note: In a real implementation, we would pass DATABASE_NAME env var
// For now, we just test the deployment mechanism
backendID = CreateTestDeployment(t, env, backendName, tarballPath)
assert.NotEmpty(t, backendID, "Backend deployment ID should not be empty")
t.Logf("✓ Go backend deployed: %s", backendName)
// Wait for deployment to become active
time.Sleep(3 * time.Second)
})
// Step 3: Test database operations
t.Run("Test database CRUD operations", func(t *testing.T) {
// INSERT
insertQuery := `INSERT INTO users (name, email) VALUES ('Bob', 'bob@example.com')`
ExecuteSQLQuery(t, env, dbName, insertQuery)
// SELECT
users := QuerySQLite(t, env, dbName, "SELECT * FROM users ORDER BY id")
require.GreaterOrEqual(t, len(users), 2, "Should have at least 2 users")
assert.Equal(t, "Alice", users[0]["name"], "First user should be Alice")
assert.Equal(t, "Bob", users[1]["name"], "Second user should be Bob")
t.Logf("✓ Database CRUD operations work")
t.Logf(" - Found %d users", len(users))
// UPDATE
updateQuery := `UPDATE users SET email = 'alice.new@example.com' WHERE name = 'Alice'`
result := ExecuteSQLQuery(t, env, dbName, updateQuery)
rowsAffected, ok := result["rows_affected"].(float64)
require.True(t, ok, "Should have rows_affected")
assert.Equal(t, float64(1), rowsAffected, "Should update 1 row")
// Verify update
updated := QuerySQLite(t, env, dbName, "SELECT email FROM users WHERE name = 'Alice'")
require.Len(t, updated, 1, "Should find Alice")
assert.Equal(t, "alice.new@example.com", updated[0]["email"], "Email should be updated")
t.Logf("✓ UPDATE operation verified")
// DELETE
deleteQuery := `DELETE FROM users WHERE name = 'Bob'`
result = ExecuteSQLQuery(t, env, dbName, deleteQuery)
rowsAffected, ok = result["rows_affected"].(float64)
require.True(t, ok, "Should have rows_affected")
assert.Equal(t, float64(1), rowsAffected, "Should delete 1 row")
// Verify deletion
remaining := QuerySQLite(t, env, dbName, "SELECT * FROM users")
assert.Equal(t, 1, len(remaining), "Should have 1 user remaining")
t.Logf("✓ DELETE operation verified")
})
// Step 4: Test backend API endpoints (if deployment is active)
t.Run("Test backend API endpoints", func(t *testing.T) {
deployment := GetDeployment(t, env, backendID)
status, ok := deployment["status"].(string)
if !ok || status != "active" {
t.Skip("Backend deployment not active, skipping API tests")
return
}
backendDomain := fmt.Sprintf("%s.orama.network", backendName)
// Test health endpoint
resp := TestDeploymentWithHostHeader(t, env, backendDomain, "/health")
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
var health map[string]interface{}
bodyBytes, _ := io.ReadAll(resp.Body)
require.NoError(t, json.Unmarshal(bodyBytes, &health), "Should decode health response")
assert.Equal(t, "healthy", health["status"], "Status should be healthy")
assert.Equal(t, "go-backend-test", health["service"], "Service name should match")
t.Logf("✓ Backend health check passed")
} else {
t.Logf("⚠ Health check returned status %d (deployment may still be starting)", resp.StatusCode)
}
// Test users API endpoint
resp2 := TestDeploymentWithHostHeader(t, env, backendDomain, "/api/users")
defer resp2.Body.Close()
if resp2.StatusCode == http.StatusOK {
var usersResp map[string]interface{}
bodyBytes, _ := io.ReadAll(resp2.Body)
require.NoError(t, json.Unmarshal(bodyBytes, &usersResp), "Should decode users response")
users, ok := usersResp["users"].([]interface{})
require.True(t, ok, "Should have users array")
assert.GreaterOrEqual(t, len(users), 3, "Should have test users")
t.Logf("✓ Backend API endpoint works")
t.Logf(" - Users endpoint returned %d users", len(users))
} else {
t.Logf("⚠ Users API returned status %d (deployment may still be starting)", resp2.StatusCode)
}
})
// Step 5: Test database backup
t.Run("Test database backup", func(t *testing.T) {
reqBody := map[string]string{"database_name": dbName}
bodyBytes, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", env.GatewayURL+"/v1/db/sqlite/backup", bytes.NewReader(bodyBytes))
req.Header.Set("Authorization", "Bearer "+env.APIKey)
req.Header.Set("Content-Type", "application/json")
resp, err := env.HTTPClient.Do(req)
require.NoError(t, err, "Should execute backup request")
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
var result map[string]interface{}
bodyBytes, _ := io.ReadAll(resp.Body)
require.NoError(t, json.Unmarshal(bodyBytes, &result), "Should decode backup response")
backupCID, ok := result["backup_cid"].(string)
require.True(t, ok, "Should have backup CID")
assert.NotEmpty(t, backupCID, "Backup CID should not be empty")
t.Logf("✓ Database backup created")
t.Logf(" - CID: %s", backupCID)
} else {
bodyBytes, _ := io.ReadAll(resp.Body)
t.Logf("⚠ Backup returned status %d: %s", resp.StatusCode, string(bodyBytes))
}
})
// Step 6: Test concurrent database queries
t.Run("Test concurrent database reads", func(t *testing.T) {
// WAL mode should allow concurrent reads
done := make(chan bool, 5)
for i := 0; i < 5; i++ {
go func(idx int) {
users := QuerySQLite(t, env, dbName, "SELECT * FROM users")
assert.GreaterOrEqual(t, len(users), 0, "Should query successfully")
done <- true
}(i)
}
// Wait for all queries to complete
for i := 0; i < 5; i++ {
select {
case <-done:
// Success
case <-time.After(10 * time.Second):
t.Fatal("Concurrent query timeout")
}
}
t.Logf("✓ Concurrent reads successful (WAL mode verified)")
})
}
func TestFullStack_StaticSite_SQLite(t *testing.T) {
env, err := LoadTestEnv()
require.NoError(t, err, "Failed to load test environment")
appName := fmt.Sprintf("fullstack-static-%d", time.Now().Unix())
frontendName := appName + "-frontend"
dbName := appName + "-db"
var frontendID string
defer func() {
if !env.SkipCleanup {
if frontendID != "" {
DeleteDeployment(t, env, frontendID)
}
DeleteSQLiteDB(t, env, dbName)
}
}()
t.Run("Deploy static site and create database", func(t *testing.T) {
// Create database
CreateSQLiteDB(t, env, dbName)
ExecuteSQLQuery(t, env, dbName, "CREATE TABLE page_views (id INTEGER PRIMARY KEY, page TEXT, count INTEGER)")
ExecuteSQLQuery(t, env, dbName, "INSERT INTO page_views (page, count) VALUES ('home', 0)")
// Deploy frontend
tarballPath := filepath.Join("../testdata/tarballs/react-vite.tar.gz")
frontendID = CreateTestDeployment(t, env, frontendName, tarballPath)
assert.NotEmpty(t, frontendID, "Frontend deployment should succeed")
t.Logf("✓ Static site deployed with SQLite backend")
// Wait for deployment
time.Sleep(2 * time.Second)
})
t.Run("Test frontend serving and database interaction", func(t *testing.T) {
frontendDomain := fmt.Sprintf("%s.orama.network", frontendName)
// Test frontend
resp := TestDeploymentWithHostHeader(t, env, frontendDomain, "/")
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode, "Frontend should serve")
body, _ := io.ReadAll(resp.Body)
assert.Contains(t, string(body), "<div id=\"root\">", "Should contain React app")
// Simulate page view tracking
ExecuteSQLQuery(t, env, dbName, "UPDATE page_views SET count = count + 1 WHERE page = 'home'")
// Verify count
views := QuerySQLite(t, env, dbName, "SELECT count FROM page_views WHERE page = 'home'")
require.Len(t, views, 1, "Should have page view record")
count, ok := views[0]["count"].(float64)
require.True(t, ok, "Count should be a number")
assert.Equal(t, float64(1), count, "Page view count should be incremented")
t.Logf("✓ Full stack integration verified")
t.Logf(" - Frontend: %s", frontendDomain)
t.Logf(" - Database: %s", dbName)
t.Logf(" - Page views tracked: %.0f", count)
})
}

View File

@ -0,0 +1,423 @@
//go:build e2e
package e2e
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNamespaceIsolation_Deployments(t *testing.T) {
// Setup two test environments with different namespaces
envA, err := LoadTestEnvWithNamespace("namespace-a-" + fmt.Sprintf("%d", time.Now().Unix()))
require.NoError(t, err, "Failed to create namespace A environment")
envB, err := LoadTestEnvWithNamespace("namespace-b-" + fmt.Sprintf("%d", time.Now().Unix()))
require.NoError(t, err, "Failed to create namespace B environment")
tarballPath := filepath.Join("../testdata/tarballs/react-vite.tar.gz")
// Create deployment in namespace-a
deploymentNameA := "test-app-ns-a"
deploymentIDA := CreateTestDeployment(t, envA, deploymentNameA, tarballPath)
defer func() {
if !envA.SkipCleanup {
DeleteDeployment(t, envA, deploymentIDA)
}
}()
// Create deployment in namespace-b
deploymentNameB := "test-app-ns-b"
deploymentIDB := CreateTestDeployment(t, envB, deploymentNameB, tarballPath)
defer func() {
if !envB.SkipCleanup {
DeleteDeployment(t, envB, deploymentIDB)
}
}()
t.Run("Namespace-A cannot list Namespace-B deployments", func(t *testing.T) {
req, _ := http.NewRequest("GET", envA.GatewayURL+"/v1/deployments/list", nil)
req.Header.Set("Authorization", "Bearer "+envA.APIKey)
resp, err := envA.HTTPClient.Do(req)
require.NoError(t, err, "Should execute request")
defer resp.Body.Close()
var result map[string]interface{}
bodyBytes, _ := io.ReadAll(resp.Body)
require.NoError(t, json.Unmarshal(bodyBytes, &result), "Should decode JSON")
deployments, ok := result["deployments"].([]interface{})
require.True(t, ok, "Deployments should be an array")
// Should only see namespace-a deployments
for _, d := range deployments {
dep, ok := d.(map[string]interface{})
if !ok {
continue
}
assert.NotEqual(t, deploymentNameB, dep["name"], "Should not see namespace-b deployment")
}
t.Logf("✓ Namespace A cannot see Namespace B deployments")
})
t.Run("Namespace-A cannot access Namespace-B deployment by ID", func(t *testing.T) {
req, _ := http.NewRequest("GET", envA.GatewayURL+"/v1/deployments/get?id="+deploymentIDB, nil)
req.Header.Set("Authorization", "Bearer "+envA.APIKey)
resp, err := envA.HTTPClient.Do(req)
require.NoError(t, err, "Should execute request")
defer resp.Body.Close()
// Should return 404 or 403
assert.Contains(t, []int{http.StatusNotFound, http.StatusForbidden}, resp.StatusCode,
"Should block cross-namespace access")
t.Logf("✓ Namespace A cannot access Namespace B deployment (status: %d)", resp.StatusCode)
})
t.Run("Namespace-A cannot delete Namespace-B deployment", func(t *testing.T) {
req, _ := http.NewRequest("DELETE", envA.GatewayURL+"/v1/deployments/delete?id="+deploymentIDB, nil)
req.Header.Set("Authorization", "Bearer "+envA.APIKey)
resp, err := envA.HTTPClient.Do(req)
require.NoError(t, err, "Should execute request")
defer resp.Body.Close()
assert.Contains(t, []int{http.StatusNotFound, http.StatusForbidden}, resp.StatusCode,
"Should block cross-namespace deletion")
// Verify deployment still exists for namespace-b
req2, _ := http.NewRequest("GET", envB.GatewayURL+"/v1/deployments/get?id="+deploymentIDB, nil)
req2.Header.Set("Authorization", "Bearer "+envB.APIKey)
resp2, err := envB.HTTPClient.Do(req2)
require.NoError(t, err, "Should execute request")
defer resp2.Body.Close()
assert.Equal(t, http.StatusOK, resp2.StatusCode, "Deployment should still exist in namespace B")
t.Logf("✓ Namespace A cannot delete Namespace B deployment")
})
}
func TestNamespaceIsolation_SQLiteDatabases(t *testing.T) {
envA, _ := LoadTestEnvWithNamespace("namespace-a-" + fmt.Sprintf("%d", time.Now().Unix()))
envB, _ := LoadTestEnvWithNamespace("namespace-b-" + fmt.Sprintf("%d", time.Now().Unix()))
// Create database in namespace-a
dbNameA := "users-db-a"
CreateSQLiteDB(t, envA, dbNameA)
defer func() {
if !envA.SkipCleanup {
DeleteSQLiteDB(t, envA, dbNameA)
}
}()
// Create database in namespace-b
dbNameB := "users-db-b"
CreateSQLiteDB(t, envB, dbNameB)
defer func() {
if !envB.SkipCleanup {
DeleteSQLiteDB(t, envB, dbNameB)
}
}()
t.Run("Namespace-A cannot list Namespace-B databases", func(t *testing.T) {
req, _ := http.NewRequest("GET", envA.GatewayURL+"/v1/db/sqlite/list", nil)
req.Header.Set("Authorization", "Bearer "+envA.APIKey)
resp, err := envA.HTTPClient.Do(req)
require.NoError(t, err, "Should execute request")
defer resp.Body.Close()
var result map[string]interface{}
bodyBytes, _ := io.ReadAll(resp.Body)
require.NoError(t, json.Unmarshal(bodyBytes, &result), "Should decode JSON")
databases, ok := result["databases"].([]interface{})
require.True(t, ok, "Databases should be an array")
for _, db := range databases {
database, ok := db.(map[string]interface{})
if !ok {
continue
}
assert.NotEqual(t, dbNameB, database["database_name"], "Should not see namespace-b database")
}
t.Logf("✓ Namespace A cannot see Namespace B databases")
})
t.Run("Namespace-A cannot query Namespace-B database", func(t *testing.T) {
reqBody := map[string]interface{}{
"database_name": dbNameB,
"query": "SELECT * FROM users",
}
bodyBytes, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", envA.GatewayURL+"/v1/db/sqlite/query", bytes.NewReader(bodyBytes))
req.Header.Set("Authorization", "Bearer "+envA.APIKey)
req.Header.Set("Content-Type", "application/json")
resp, err := envA.HTTPClient.Do(req)
require.NoError(t, err, "Should execute request")
defer resp.Body.Close()
assert.Equal(t, http.StatusNotFound, resp.StatusCode, "Should block cross-namespace query")
t.Logf("✓ Namespace A cannot query Namespace B database")
})
t.Run("Namespace-A cannot backup Namespace-B database", func(t *testing.T) {
reqBody := map[string]string{"database_name": dbNameB}
bodyBytes, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", envA.GatewayURL+"/v1/db/sqlite/backup", bytes.NewReader(bodyBytes))
req.Header.Set("Authorization", "Bearer "+envA.APIKey)
req.Header.Set("Content-Type", "application/json")
resp, err := envA.HTTPClient.Do(req)
require.NoError(t, err, "Should execute request")
defer resp.Body.Close()
assert.Equal(t, http.StatusNotFound, resp.StatusCode, "Should block cross-namespace backup")
t.Logf("✓ Namespace A cannot backup Namespace B database")
})
}
func TestNamespaceIsolation_IPFSContent(t *testing.T) {
envA, _ := LoadTestEnvWithNamespace("namespace-a-" + fmt.Sprintf("%d", time.Now().Unix()))
envB, _ := LoadTestEnvWithNamespace("namespace-b-" + fmt.Sprintf("%d", time.Now().Unix()))
// Upload file in namespace-a
cidA := UploadTestFile(t, envA, "test-file-a.txt", "Content from namespace A")
defer func() {
if !envA.SkipCleanup {
UnpinFile(t, envA, cidA)
}
}()
t.Run("Namespace-B cannot GET Namespace-A IPFS content", func(t *testing.T) {
// This tests application-level access control
// IPFS content is globally accessible by CID, but our handlers should enforce namespace
req, _ := http.NewRequest("GET", envB.GatewayURL+"/v1/storage/get?cid="+cidA, nil)
req.Header.Set("Authorization", "Bearer "+envB.APIKey)
resp, err := envB.HTTPClient.Do(req)
require.NoError(t, err, "Should execute request")
defer resp.Body.Close()
// Should return 403 or 404 (namespace doesn't own this CID)
assert.Contains(t, []int{http.StatusNotFound, http.StatusForbidden}, resp.StatusCode,
"Should block cross-namespace IPFS GET")
t.Logf("✓ Namespace B cannot GET Namespace A IPFS content (status: %d)", resp.StatusCode)
})
t.Run("Namespace-B cannot PIN Namespace-A IPFS content", func(t *testing.T) {
reqBody := map[string]string{
"cid": cidA,
"name": "stolen-content",
}
bodyBytes, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", envB.GatewayURL+"/v1/storage/pin", bytes.NewReader(bodyBytes))
req.Header.Set("Authorization", "Bearer "+envB.APIKey)
req.Header.Set("Content-Type", "application/json")
resp, err := envB.HTTPClient.Do(req)
require.NoError(t, err, "Should execute request")
defer resp.Body.Close()
assert.Contains(t, []int{http.StatusNotFound, http.StatusForbidden}, resp.StatusCode,
"Should block cross-namespace PIN")
t.Logf("✓ Namespace B cannot PIN Namespace A IPFS content (status: %d)", resp.StatusCode)
})
t.Run("Namespace-B cannot UNPIN Namespace-A IPFS content", func(t *testing.T) {
reqBody := map[string]string{"cid": cidA}
bodyBytes, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", envB.GatewayURL+"/v1/storage/unpin", bytes.NewReader(bodyBytes))
req.Header.Set("Authorization", "Bearer "+envB.APIKey)
req.Header.Set("Content-Type", "application/json")
resp, err := envB.HTTPClient.Do(req)
require.NoError(t, err, "Should execute request")
defer resp.Body.Close()
assert.Contains(t, []int{http.StatusNotFound, http.StatusForbidden}, resp.StatusCode,
"Should block cross-namespace UNPIN")
t.Logf("✓ Namespace B cannot UNPIN Namespace A IPFS content (status: %d)", resp.StatusCode)
})
t.Run("Namespace-A can list only their own IPFS pins", func(t *testing.T) {
req, _ := http.NewRequest("GET", envA.GatewayURL+"/v1/storage/pins", nil)
req.Header.Set("Authorization", "Bearer "+envA.APIKey)
resp, err := envA.HTTPClient.Do(req)
require.NoError(t, err, "Should execute request")
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode, "Should list pins successfully")
var pins []map[string]interface{}
bodyBytes, _ := io.ReadAll(resp.Body)
require.NoError(t, json.Unmarshal(bodyBytes, &pins), "Should decode pins")
// Should see their own pin
foundOwn := false
for _, pin := range pins {
if cid, ok := pin["cid"].(string); ok && cid == cidA {
foundOwn = true
break
}
}
assert.True(t, foundOwn, "Should see own pins")
t.Logf("✓ Namespace A can list only their own pins")
})
}
func TestNamespaceIsolation_OlricCache(t *testing.T) {
envA, _ := LoadTestEnvWithNamespace("namespace-a-" + fmt.Sprintf("%d", time.Now().Unix()))
envB, _ := LoadTestEnvWithNamespace("namespace-b-" + fmt.Sprintf("%d", time.Now().Unix()))
keyA := "user-session-123"
valueA := `{"user_id": "alice", "token": "secret-token-a"}`
t.Run("Namespace-A sets cache key", func(t *testing.T) {
reqBody := map[string]interface{}{
"key": keyA,
"value": valueA,
"ttl": 300,
}
bodyBytes, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", envA.GatewayURL+"/v1/cache/set", bytes.NewReader(bodyBytes))
req.Header.Set("Authorization", "Bearer "+envA.APIKey)
req.Header.Set("Content-Type", "application/json")
resp, err := envA.HTTPClient.Do(req)
require.NoError(t, err, "Should execute request")
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode, "Should set cache key successfully")
t.Logf("✓ Namespace A set cache key")
})
t.Run("Namespace-B cannot GET Namespace-A cache key", func(t *testing.T) {
req, _ := http.NewRequest("GET", envB.GatewayURL+"/v1/cache/get?key="+keyA, nil)
req.Header.Set("Authorization", "Bearer "+envB.APIKey)
resp, err := envB.HTTPClient.Do(req)
require.NoError(t, err, "Should execute request")
defer resp.Body.Close()
// Should return 404 (key doesn't exist in namespace-b)
assert.Equal(t, http.StatusNotFound, resp.StatusCode, "Should not find key in different namespace")
t.Logf("✓ Namespace B cannot GET Namespace A cache key")
})
t.Run("Namespace-B cannot DELETE Namespace-A cache key", func(t *testing.T) {
reqBody := map[string]string{"key": keyA}
bodyBytes, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", envB.GatewayURL+"/v1/cache/delete", bytes.NewReader(bodyBytes))
req.Header.Set("Authorization", "Bearer "+envB.APIKey)
req.Header.Set("Content-Type", "application/json")
resp, err := envB.HTTPClient.Do(req)
require.NoError(t, err, "Should execute request")
defer resp.Body.Close()
// Should return 404 or success (key doesn't exist in their namespace)
assert.Contains(t, []int{http.StatusOK, http.StatusNotFound}, resp.StatusCode)
// Verify key still exists for namespace-a
req2, _ := http.NewRequest("GET", envA.GatewayURL+"/v1/cache/get?key="+keyA, nil)
req2.Header.Set("Authorization", "Bearer "+envA.APIKey)
resp2, err := envA.HTTPClient.Do(req2)
require.NoError(t, err, "Should execute request")
defer resp2.Body.Close()
assert.Equal(t, http.StatusOK, resp2.StatusCode, "Key should still exist in namespace A")
var result map[string]interface{}
bodyBytes2, _ := io.ReadAll(resp2.Body)
require.NoError(t, json.Unmarshal(bodyBytes2, &result), "Should decode result")
assert.Equal(t, valueA, result["value"], "Value should match")
t.Logf("✓ Namespace B cannot DELETE Namespace A cache key")
})
t.Run("Namespace-B can set same key name in their namespace", func(t *testing.T) {
// Same key name, different namespace should be allowed
valueB := `{"user_id": "bob", "token": "secret-token-b"}`
reqBody := map[string]interface{}{
"key": keyA, // Same key name as namespace-a
"value": valueB,
"ttl": 300,
}
bodyBytes, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", envB.GatewayURL+"/v1/cache/set", bytes.NewReader(bodyBytes))
req.Header.Set("Authorization", "Bearer "+envB.APIKey)
req.Header.Set("Content-Type", "application/json")
resp, err := envB.HTTPClient.Do(req)
require.NoError(t, err, "Should execute request")
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode, "Should set key in namespace B")
// Verify namespace-a still has their value
req2, _ := http.NewRequest("GET", envA.GatewayURL+"/v1/cache/get?key="+keyA, nil)
req2.Header.Set("Authorization", "Bearer "+envA.APIKey)
resp2, _ := envA.HTTPClient.Do(req2)
defer resp2.Body.Close()
var resultA map[string]interface{}
bodyBytesA, _ := io.ReadAll(resp2.Body)
require.NoError(t, json.Unmarshal(bodyBytesA, &resultA), "Should decode result A")
assert.Equal(t, valueA, resultA["value"], "Namespace A value should be unchanged")
// Verify namespace-b has their different value
req3, _ := http.NewRequest("GET", envB.GatewayURL+"/v1/cache/get?key="+keyA, nil)
req3.Header.Set("Authorization", "Bearer "+envB.APIKey)
resp3, _ := envB.HTTPClient.Do(req3)
defer resp3.Body.Close()
var resultB map[string]interface{}
bodyBytesB, _ := io.ReadAll(resp3.Body)
require.NoError(t, json.Unmarshal(bodyBytesB, &resultB), "Should decode result B")
assert.Equal(t, valueB, resultB["value"], "Namespace B value should be different")
t.Logf("✓ Namespace B can set same key name independently")
t.Logf(" - Namespace A value: %s", valueA)
t.Logf(" - Namespace B value: %s", valueB)
})
}

View File

@ -6,7 +6,7 @@ BEGIN;
-- DNS records table for dynamic DNS management
CREATE TABLE IF NOT EXISTS dns_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
fqdn TEXT NOT NULL UNIQUE, -- Fully qualified domain name (e.g., myapp.node-7prvNa.debros.network)
fqdn TEXT NOT NULL UNIQUE, -- Fully qualified domain name (e.g., myapp.node-7prvNa.orama.network)
record_type TEXT NOT NULL DEFAULT 'A', -- DNS record type: A, AAAA, CNAME, TXT
value TEXT NOT NULL, -- IP address or target value
ttl INTEGER NOT NULL DEFAULT 300, -- Time to live in seconds
@ -53,17 +53,17 @@ CREATE TABLE IF NOT EXISTS reserved_domains (
-- Seed reserved domains
INSERT INTO reserved_domains (domain, reason) VALUES
('api.debros.network', 'API gateway endpoint'),
('www.debros.network', 'Marketing website'),
('admin.debros.network', 'Admin panel'),
('ns1.debros.network', 'Nameserver 1'),
('ns2.debros.network', 'Nameserver 2'),
('ns3.debros.network', 'Nameserver 3'),
('ns4.debros.network', 'Nameserver 4'),
('mail.debros.network', 'Email service'),
('cdn.debros.network', 'Content delivery'),
('docs.debros.network', 'Documentation'),
('status.debros.network', 'Status page')
('api.orama.network', 'API gateway endpoint'),
('www.orama.network', 'Marketing website'),
('admin.orama.network', 'Admin panel'),
('ns1.orama.network', 'Nameserver 1'),
('ns2.orama.network', 'Nameserver 2'),
('ns3.orama.network', 'Nameserver 3'),
('ns4.orama.network', 'Nameserver 4'),
('mail.orama.network', 'Email service'),
('cdn.orama.network', 'Content delivery'),
('docs.orama.network', 'Documentation'),
('status.orama.network', 'Status page')
ON CONFLICT(domain) DO NOTHING;
-- Mark migration as applied

View File

@ -83,7 +83,7 @@ CREATE TABLE IF NOT EXISTS deployment_domains (
id TEXT PRIMARY KEY, -- UUID
deployment_id TEXT NOT NULL,
namespace TEXT NOT NULL,
domain TEXT NOT NULL UNIQUE, -- Full domain (e.g., myapp.debros.network or custom)
domain TEXT NOT NULL UNIQUE, -- Full domain (e.g., myapp.orama.network or custom)
routing_type TEXT NOT NULL DEFAULT 'balanced', -- 'balanced' or 'node_specific'
node_id TEXT, -- For node_specific routing
is_custom BOOLEAN DEFAULT FALSE, -- True for user's own domain

View File

@ -95,7 +95,7 @@ func requestAPIKeyFromGateway(gatewayURL, wallet, namespace string) (string, err
endpoint := gatewayURL + "/v1/auth/simple-key"
// Extract domain from URL for TLS configuration
// This uses tlsutil which handles Let's Encrypt staging certificates for *.debros.network
// This uses tlsutil which handles Let's Encrypt staging certificates for *.orama.network
domain := extractDomainFromURL(gatewayURL)
client := tlsutil.NewHTTPClientForDomain(30*time.Second, domain)

View File

@ -179,11 +179,11 @@ func (cm *CertificateManager) generateNodeCertificate(hostname string, caCertPEM
DNSNames: []string{hostname},
}
// Add wildcard support if hostname contains *.debros.network
if hostname == "*.debros.network" {
template.DNSNames = []string{"*.debros.network", "debros.network"}
} else if hostname == "debros.network" {
template.DNSNames = []string{"*.debros.network", "debros.network"}
// Add wildcard support if hostname contains *.orama.network
if hostname == "*.orama.network" {
template.DNSNames = []string{"*.orama.network", "orama.network"}
} else if hostname == "orama.network" {
template.DNSNames = []string{"*.orama.network", "orama.network"}
}
// Try to parse as IP address for IP-based certificates
@ -254,4 +254,3 @@ func (cm *CertificateManager) parseCACertificate(caCertPEM, caKeyPEM []byte) (*x
func LoadTLSCertificate(certPEM, keyPEM []byte) (tls.Certificate, error) {
return tls.X509KeyPair(certPEM, keyPEM)
}

View File

@ -192,7 +192,7 @@ func promptForGatewayURL() string {
return "http://localhost:6001"
}
fmt.Print("Enter node domain (e.g., node-hk19de.debros.network): ")
fmt.Print("Enter node domain (e.g., node-hk19de.orama.network): ")
domain, _ := reader.ReadString('\n')
domain = strings.TrimSpace(domain)

View File

@ -439,7 +439,7 @@ func getAPIURL() string {
if url := os.Getenv("ORAMA_API_URL"); url != "" {
return url
}
return "https://gateway.debros.network"
return "https://gateway.orama.network"
}
func getAuthToken() (string, error) {

View File

@ -387,7 +387,7 @@ func getAPIURL() string {
if url := os.Getenv("ORAMA_API_URL"); url != "" {
return url
}
return "https://gateway.debros.network"
return "https://gateway.orama.network"
}
func getAuthToken() (string, error) {

View File

@ -8,7 +8,7 @@ The plugin provides:
- **Dynamic DNS Records**: Queries RQLite for DNS records in real-time
- **Caching**: In-memory cache to reduce database load
- **Health Monitoring**: Periodic health checks of RQLite connection
- **Wildcard Support**: Handles wildcard DNS patterns (e.g., `*.node-xyz.debros.network`)
- **Wildcard Support**: Handles wildcard DNS patterns (e.g., `*.node-xyz.orama.network`)
## Building CoreDNS with RQLite Plugin
@ -146,7 +146,7 @@ sudo systemctl start coredns
The Corefile at `/etc/coredns/Corefile` configures CoreDNS behavior:
```corefile
debros.network {
orama.network {
rqlite {
dsn http://localhost:5001 # RQLite HTTP endpoint
refresh 10s # Health check interval
@ -196,7 +196,7 @@ curl -XPOST 'http://localhost:5001/db/execute' \
-H 'Content-Type: application/json' \
-d '[
["INSERT INTO dns_records (fqdn, record_type, value, ttl, namespace, created_by, is_active) VALUES (?, ?, ?, ?, ?, ?, ?)",
"test.debros.network.", "A", "1.2.3.4", 300, "test", "system", true]
"test.orama.network.", "A", "1.2.3.4", 300, "test", "system", true]
]'
```
@ -204,14 +204,14 @@ curl -XPOST 'http://localhost:5001/db/execute' \
```bash
# Query local CoreDNS
dig @localhost test.debros.network
dig @localhost test.orama.network
# Expected output:
# ;; ANSWER SECTION:
# test.debros.network. 300 IN A 1.2.3.4
# test.orama.network. 300 IN A 1.2.3.4
# Query from remote machine
dig @<node-ip> test.debros.network
dig @<node-ip> test.orama.network
```
### 3. Test Wildcard
@ -222,12 +222,12 @@ curl -XPOST 'http://localhost:5001/db/execute' \
-H 'Content-Type: application/json' \
-d '[
["INSERT INTO dns_records (fqdn, record_type, value, ttl, namespace, created_by, is_active) VALUES (?, ?, ?, ?, ?, ?, ?)",
"*.node-abc123.debros.network.", "A", "1.2.3.4", 300, "test", "system", true]
"*.node-abc123.orama.network.", "A", "1.2.3.4", 300, "test", "system", true]
]'
# Test wildcard resolution
dig @localhost app1.node-abc123.debros.network
dig @localhost app2.node-abc123.debros.network
dig @localhost app1.node-abc123.orama.network
dig @localhost app2.node-abc123.orama.network
```
### 4. Check Health
@ -295,7 +295,7 @@ sudo ufw status | grep 5001
sudo netstat -tulpn | grep :53
# 2. Test local query
dig @127.0.0.1 test.debros.network
dig @127.0.0.1 test.orama.network
# 3. Check logs for errors
sudo journalctl -u coredns --since "5 minutes ago"
@ -327,38 +327,38 @@ Install CoreDNS on all 4 nameserver nodes (ns1-ns4).
### 2. Configure Registrar
At your domain registrar, set NS records for `debros.network`:
At your domain registrar, set NS records for `orama.network`:
```
debros.network. IN NS ns1.debros.network.
debros.network. IN NS ns2.debros.network.
debros.network. IN NS ns3.debros.network.
debros.network. IN NS ns4.debros.network.
orama.network. IN NS ns1.orama.network.
orama.network. IN NS ns2.orama.network.
orama.network. IN NS ns3.orama.network.
orama.network. IN NS ns4.orama.network.
```
Add glue records:
```
ns1.debros.network. IN A <node-1-ip>
ns2.debros.network. IN A <node-2-ip>
ns3.debros.network. IN A <node-3-ip>
ns4.debros.network. IN A <node-4-ip>
ns1.orama.network. IN A <node-1-ip>
ns2.orama.network. IN A <node-2-ip>
ns3.orama.network. IN A <node-3-ip>
ns4.orama.network. IN A <node-4-ip>
```
### 3. Verify Propagation
```bash
# Check NS records
dig NS debros.network
dig NS orama.network
# Check from public DNS
dig @8.8.8.8 test.debros.network
dig @8.8.8.8 test.orama.network
# Check from all nameservers
dig @ns1.debros.network test.debros.network
dig @ns2.debros.network test.debros.network
dig @ns3.debros.network test.debros.network
dig @ns4.debros.network test.debros.network
dig @ns1.orama.network test.orama.network
dig @ns2.orama.network test.orama.network
dig @ns3.orama.network test.orama.network
dig @ns4.orama.network test.orama.network
```
### 4. Monitor

View File

@ -2,7 +2,6 @@ package rqlite
import (
"context"
"fmt"
"time"
"github.com/coredns/coredns/plugin"
@ -13,11 +12,11 @@ import (
// RQLitePlugin implements the CoreDNS plugin interface
type RQLitePlugin struct {
Next plugin.Handler
logger *zap.Logger
Next plugin.Handler
logger *zap.Logger
backend *Backend
cache *Cache
zones []string
cache *Cache
zones []string
}
// Name returns the plugin name
@ -110,7 +109,7 @@ func (p *RQLitePlugin) isOurZone(qname string) bool {
}
// getWildcardName extracts the wildcard pattern for a given name
// e.g., myapp.node-7prvNa.debros.network -> *.node-7prvNa.debros.network
// e.g., myapp.node-7prvNa.orama.network -> *.node-7prvNa.orama.network
func (p *RQLitePlugin) getWildcardName(qname string) string {
labels := dns.SplitDomainName(qname)
if len(labels) < 3 {

View File

@ -62,8 +62,8 @@ func (h *DomainHandler) HandleAddDomain(w http.ResponseWriter, r *http.Request)
}
// Check if domain is reserved
if strings.HasSuffix(domain, ".debros.network") {
http.Error(w, "Cannot use .debros.network domains as custom domains", http.StatusBadRequest)
if strings.HasSuffix(domain, ".orama.network") {
http.Error(w, "Cannot use .orama.network domains as custom domains", http.StatusBadRequest)
return
}
@ -165,8 +165,8 @@ func (h *DomainHandler) HandleVerifyDomain(w http.ResponseWriter, r *http.Reques
// Get domain record
type domainRow struct {
DeploymentID string `db:"deployment_id"`
VerificationToken string `db:"verification_token"`
DeploymentID string `db:"deployment_id"`
VerificationToken string `db:"verification_token"`
VerificationStatus string `db:"verification_status"`
}
@ -258,9 +258,9 @@ func (h *DomainHandler) HandleListDomains(w http.ResponseWriter, r *http.Request
// Query domains
type domainRow struct {
Domain string `db:"domain"`
VerificationStatus string `db:"verification_status"`
CreatedAt time.Time `db:"created_at"`
Domain string `db:"domain"`
VerificationStatus string `db:"verification_status"`
CreatedAt time.Time `db:"created_at"`
VerifiedAt *time.Time `db:"verified_at"`
}

View File

@ -0,0 +1,398 @@
package deployments
import (
"bytes"
"context"
"database/sql"
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"
"github.com/DeBrosOfficial/network/pkg/deployments"
"github.com/DeBrosOfficial/network/pkg/ipfs"
"go.uber.org/zap"
)
// TestStaticHandler_Upload tests uploading a static site tarball to IPFS
func TestStaticHandler_Upload(t *testing.T) {
// Create mock IPFS client
mockIPFS := &mockIPFSClient{
AddFunc: func(ctx context.Context, r io.Reader, filename string) (*ipfs.AddResponse, error) {
// Verify we're receiving data
data, err := io.ReadAll(r)
if err != nil {
t.Errorf("Failed to read upload data: %v", err)
}
if len(data) == 0 {
t.Error("Expected non-empty upload data")
}
return &ipfs.AddResponse{Cid: "QmTestCID123456789"}, nil
},
}
// Create mock RQLite client with basic implementations
mockDB := &mockRQLiteClient{
QueryFunc: func(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
// For dns_nodes query, return mock active node
if strings.Contains(query, "dns_nodes") {
// Use reflection to set the slice
destValue := reflect.ValueOf(dest)
if destValue.Kind() == reflect.Ptr {
sliceValue := destValue.Elem()
if sliceValue.Kind() == reflect.Slice {
// Create one element
elemType := sliceValue.Type().Elem()
newElem := reflect.New(elemType).Elem()
// Set ID field
idField := newElem.FieldByName("ID")
if idField.IsValid() && idField.CanSet() {
idField.SetString("node-test123")
}
// Append to slice
sliceValue.Set(reflect.Append(sliceValue, newElem))
}
}
}
return nil
},
ExecFunc: func(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
return nil, nil
},
}
// Create port allocator and home node manager with mock DB
portAlloc := deployments.NewPortAllocator(mockDB, zap.NewNop())
homeNodeMgr := deployments.NewHomeNodeManager(mockDB, portAlloc, zap.NewNop())
// Create handler
service := &DeploymentService{
db: mockDB,
homeNodeManager: homeNodeMgr,
portAllocator: portAlloc,
logger: zap.NewNop(),
}
handler := NewStaticDeploymentHandler(service, mockIPFS, zap.NewNop())
// Create multipart form with tarball
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
// Add name field
writer.WriteField("name", "test-app")
// Add namespace field
writer.WriteField("namespace", "test-namespace")
// Add tarball file
part, err := writer.CreateFormFile("tarball", "app.tar.gz")
if err != nil {
t.Fatalf("Failed to create form file: %v", err)
}
part.Write([]byte("fake tarball data"))
writer.Close()
// Create request
req := httptest.NewRequest("POST", "/v1/deployments/static/upload", body)
req.Header.Set("Content-Type", writer.FormDataContentType())
ctx := context.WithValue(req.Context(), "namespace", "test-namespace")
req = req.WithContext(ctx)
// Create response recorder
rr := httptest.NewRecorder()
// Call handler
handler.HandleUpload(rr, req)
// Check response
if rr.Code != http.StatusOK && rr.Code != http.StatusCreated {
t.Errorf("Expected status 200 or 201, got %d", rr.Code)
t.Logf("Response body: %s", rr.Body.String())
}
}
// TestStaticHandler_Upload_InvalidTarball tests that malformed tarballs are rejected
func TestStaticHandler_Upload_InvalidTarball(t *testing.T) {
// Create mock IPFS client
mockIPFS := &mockIPFSClient{}
// Create mock RQLite client
mockDB := &mockRQLiteClient{}
// Create port allocator and home node manager with mock DB
portAlloc := deployments.NewPortAllocator(mockDB, zap.NewNop())
homeNodeMgr := deployments.NewHomeNodeManager(mockDB, portAlloc, zap.NewNop())
// Create handler
service := &DeploymentService{
db: mockDB,
homeNodeManager: homeNodeMgr,
portAllocator: portAlloc,
logger: zap.NewNop(),
}
handler := NewStaticDeploymentHandler(service, mockIPFS, zap.NewNop())
// Create request without tarball field
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
writer.WriteField("name", "test-app")
writer.Close()
req := httptest.NewRequest("POST", "/v1/deployments/static/upload", body)
req.Header.Set("Content-Type", writer.FormDataContentType())
ctx := context.WithValue(req.Context(), "namespace", "test-namespace")
req = req.WithContext(ctx)
rr := httptest.NewRecorder()
// Call handler
handler.HandleUpload(rr, req)
// Should return error (400 or 500)
if rr.Code == http.StatusOK || rr.Code == http.StatusCreated {
t.Errorf("Expected error status, got %d", rr.Code)
}
}
// TestStaticHandler_Serve tests serving static files from IPFS
func TestStaticHandler_Serve(t *testing.T) {
testContent := "<html><body>Test</body></html>"
// Create mock IPFS client that returns test content
mockIPFS := &mockIPFSClient{
GetFunc: func(ctx context.Context, path, ipfsAPIURL string) (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader(testContent)), nil
},
}
// Create mock RQLite client
mockDB := &mockRQLiteClient{}
// Create port allocator and home node manager with mock DB
portAlloc := deployments.NewPortAllocator(mockDB, zap.NewNop())
homeNodeMgr := deployments.NewHomeNodeManager(mockDB, portAlloc, zap.NewNop())
// Create handler
service := &DeploymentService{
db: mockDB,
homeNodeManager: homeNodeMgr,
portAllocator: portAlloc,
logger: zap.NewNop(),
}
handler := NewStaticDeploymentHandler(service, mockIPFS, zap.NewNop())
// Create test deployment
deployment := &deployments.Deployment{
ID: "test-id",
ContentCID: "QmTestCID",
Type: deployments.DeploymentTypeStatic,
Status: deployments.DeploymentStatusActive,
Name: "test-app",
Namespace: "test-namespace",
}
// Create request
req := httptest.NewRequest("GET", "/", nil)
req.Host = "test-app.orama.network"
rr := httptest.NewRecorder()
// Call handler
handler.HandleServe(rr, req, deployment)
// Check response
if rr.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", rr.Code)
}
// Check content
body := rr.Body.String()
if body != testContent {
t.Errorf("Expected %q, got %q", testContent, body)
}
}
// TestStaticHandler_Serve_CSS tests that CSS files get correct Content-Type
func TestStaticHandler_Serve_CSS(t *testing.T) {
testContent := "body { color: red; }"
mockIPFS := &mockIPFSClient{
GetFunc: func(ctx context.Context, path, ipfsAPIURL string) (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader(testContent)), nil
},
}
mockDB := &mockRQLiteClient{}
service := &DeploymentService{
db: mockDB,
logger: zap.NewNop(),
}
handler := NewStaticDeploymentHandler(service, mockIPFS, zap.NewNop())
deployment := &deployments.Deployment{
ID: "test-id",
ContentCID: "QmTestCID",
Type: deployments.DeploymentTypeStatic,
Status: deployments.DeploymentStatusActive,
Name: "test-app",
Namespace: "test-namespace",
}
req := httptest.NewRequest("GET", "/style.css", nil)
req.Host = "test-app.orama.network"
rr := httptest.NewRecorder()
handler.HandleServe(rr, req, deployment)
// Check Content-Type header
contentType := rr.Header().Get("Content-Type")
if !strings.Contains(contentType, "text/css") {
t.Errorf("Expected Content-Type to contain 'text/css', got %q", contentType)
}
}
// TestStaticHandler_Serve_JS tests that JavaScript files get correct Content-Type
func TestStaticHandler_Serve_JS(t *testing.T) {
testContent := "console.log('test');"
mockIPFS := &mockIPFSClient{
GetFunc: func(ctx context.Context, path, ipfsAPIURL string) (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader(testContent)), nil
},
}
mockDB := &mockRQLiteClient{}
service := &DeploymentService{
db: mockDB,
logger: zap.NewNop(),
}
handler := NewStaticDeploymentHandler(service, mockIPFS, zap.NewNop())
deployment := &deployments.Deployment{
ID: "test-id",
ContentCID: "QmTestCID",
Type: deployments.DeploymentTypeStatic,
Status: deployments.DeploymentStatusActive,
Name: "test-app",
Namespace: "test-namespace",
}
req := httptest.NewRequest("GET", "/app.js", nil)
req.Host = "test-app.orama.network"
rr := httptest.NewRecorder()
handler.HandleServe(rr, req, deployment)
// Check Content-Type header
contentType := rr.Header().Get("Content-Type")
if !strings.Contains(contentType, "application/javascript") {
t.Errorf("Expected Content-Type to contain 'application/javascript', got %q", contentType)
}
}
// TestStaticHandler_Serve_SPAFallback tests that unknown paths fall back to index.html
func TestStaticHandler_Serve_SPAFallback(t *testing.T) {
indexContent := "<html><body>SPA</body></html>"
callCount := 0
mockIPFS := &mockIPFSClient{
GetFunc: func(ctx context.Context, path, ipfsAPIURL string) (io.ReadCloser, error) {
callCount++
// First call: return error for /unknown-route
// Second call: return index.html
if callCount == 1 {
return nil, io.EOF // Simulate file not found
}
return io.NopCloser(strings.NewReader(indexContent)), nil
},
}
mockDB := &mockRQLiteClient{}
service := &DeploymentService{
db: mockDB,
logger: zap.NewNop(),
}
handler := NewStaticDeploymentHandler(service, mockIPFS, zap.NewNop())
deployment := &deployments.Deployment{
ID: "test-id",
ContentCID: "QmTestCID",
Type: deployments.DeploymentTypeStatic,
Status: deployments.DeploymentStatusActive,
Name: "test-app",
Namespace: "test-namespace",
}
req := httptest.NewRequest("GET", "/unknown-route", nil)
req.Host = "test-app.orama.network"
rr := httptest.NewRecorder()
handler.HandleServe(rr, req, deployment)
// Should return index.html content
if rr.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", rr.Code)
}
body := rr.Body.String()
if body != indexContent {
t.Errorf("Expected index.html content, got %q", body)
}
// Verify IPFS was called twice (first for route, then for index.html)
if callCount < 2 {
t.Errorf("Expected at least 2 IPFS calls for SPA fallback, got %d", callCount)
}
}
// TestListHandler_AllDeployments tests listing all deployments for a namespace
func TestListHandler_AllDeployments(t *testing.T) {
mockDB := &mockRQLiteClient{
QueryFunc: func(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
// The handler uses a local deploymentRow struct type, not deployments.Deployment
// So we just return nil and let the test verify basic flow
return nil
},
}
// Create port allocator and home node manager with mock DB
portAlloc := deployments.NewPortAllocator(mockDB, zap.NewNop())
homeNodeMgr := deployments.NewHomeNodeManager(mockDB, portAlloc, zap.NewNop())
service := &DeploymentService{
db: mockDB,
homeNodeManager: homeNodeMgr,
portAllocator: portAlloc,
logger: zap.NewNop(),
}
handler := NewListHandler(service, zap.NewNop())
req := httptest.NewRequest("GET", "/v1/deployments/list", nil)
ctx := context.WithValue(req.Context(), "namespace", "test-namespace")
req = req.WithContext(ctx)
rr := httptest.NewRecorder()
handler.HandleList(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", rr.Code)
t.Logf("Response body: %s", rr.Body.String())
}
// Check that response is valid JSON
body := rr.Body.String()
if !strings.Contains(body, "namespace") || !strings.Contains(body, "deployments") {
t.Errorf("Expected response to contain namespace and deployments fields, got: %s", body)
}
}

View File

@ -29,18 +29,18 @@ func (h *ListHandler) HandleList(w http.ResponseWriter, r *http.Request) {
namespace := ctx.Value("namespace").(string)
type deploymentRow struct {
ID string `db:"id"`
Namespace string `db:"namespace"`
Name string `db:"name"`
Type string `db:"type"`
Version int `db:"version"`
Status string `db:"status"`
ContentCID string `db:"content_cid"`
HomeNodeID string `db:"home_node_id"`
Port int `db:"port"`
Subdomain string `db:"subdomain"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
ID string `db:"id"`
Namespace string `db:"namespace"`
Name string `db:"name"`
Type string `db:"type"`
Version int `db:"version"`
Status string `db:"status"`
ContentCID string `db:"content_cid"`
HomeNodeID string `db:"home_node_id"`
Port int `db:"port"`
Subdomain string `db:"subdomain"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
var rows []deploymentRow
@ -61,10 +61,10 @@ func (h *ListHandler) HandleList(w http.ResponseWriter, r *http.Request) {
deployments := make([]map[string]interface{}, len(rows))
for i, row := range rows {
urls := []string{
"https://" + row.Name + "." + row.HomeNodeID + ".debros.network",
"https://" + row.Name + "." + row.HomeNodeID + ".orama.network",
}
if row.Subdomain != "" {
urls = append(urls, "https://"+row.Subdomain+".debros.network")
urls = append(urls, "https://"+row.Subdomain+".orama.network")
}
deployments[i] = map[string]interface{}{

View File

@ -0,0 +1,239 @@
package deployments
import (
"context"
"database/sql"
"io"
"github.com/DeBrosOfficial/network/pkg/deployments"
"github.com/DeBrosOfficial/network/pkg/ipfs"
"github.com/DeBrosOfficial/network/pkg/rqlite"
)
// mockIPFSClient implements a mock IPFS client for testing
type mockIPFSClient struct {
AddFunc func(ctx context.Context, r io.Reader, filename string) (*ipfs.AddResponse, error)
GetFunc func(ctx context.Context, path, ipfsAPIURL string) (io.ReadCloser, error)
PinFunc func(ctx context.Context, cid, name string, replicationFactor int) (*ipfs.PinResponse, error)
PinStatusFunc func(ctx context.Context, cid string) (*ipfs.PinStatus, error)
UnpinFunc func(ctx context.Context, cid string) error
HealthFunc func(ctx context.Context) error
GetPeerFunc func(ctx context.Context) (int, error)
CloseFunc func(ctx context.Context) error
}
func (m *mockIPFSClient) Add(ctx context.Context, r io.Reader, filename string) (*ipfs.AddResponse, error) {
if m.AddFunc != nil {
return m.AddFunc(ctx, r, filename)
}
return &ipfs.AddResponse{Cid: "QmTestCID123456789"}, nil
}
func (m *mockIPFSClient) Get(ctx context.Context, cid, ipfsAPIURL string) (io.ReadCloser, error) {
if m.GetFunc != nil {
return m.GetFunc(ctx, cid, ipfsAPIURL)
}
return io.NopCloser(nil), nil
}
func (m *mockIPFSClient) Pin(ctx context.Context, cid, name string, replicationFactor int) (*ipfs.PinResponse, error) {
if m.PinFunc != nil {
return m.PinFunc(ctx, cid, name, replicationFactor)
}
return &ipfs.PinResponse{}, nil
}
func (m *mockIPFSClient) PinStatus(ctx context.Context, cid string) (*ipfs.PinStatus, error) {
if m.PinStatusFunc != nil {
return m.PinStatusFunc(ctx, cid)
}
return &ipfs.PinStatus{}, nil
}
func (m *mockIPFSClient) Unpin(ctx context.Context, cid string) error {
if m.UnpinFunc != nil {
return m.UnpinFunc(ctx, cid)
}
return nil
}
func (m *mockIPFSClient) Health(ctx context.Context) error {
if m.HealthFunc != nil {
return m.HealthFunc(ctx)
}
return nil
}
func (m *mockIPFSClient) GetPeerCount(ctx context.Context) (int, error) {
if m.GetPeerFunc != nil {
return m.GetPeerFunc(ctx)
}
return 5, nil
}
func (m *mockIPFSClient) Close(ctx context.Context) error {
if m.CloseFunc != nil {
return m.CloseFunc(ctx)
}
return nil
}
// mockRQLiteClient implements a mock RQLite client for testing
type mockRQLiteClient struct {
QueryFunc func(ctx context.Context, dest interface{}, query string, args ...interface{}) error
ExecFunc func(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
FindByFunc func(ctx context.Context, dest interface{}, table string, criteria map[string]interface{}, opts ...rqlite.FindOption) error
FindOneFunc func(ctx context.Context, dest interface{}, table string, criteria map[string]interface{}, opts ...rqlite.FindOption) error
SaveFunc func(ctx context.Context, entity interface{}) error
RemoveFunc func(ctx context.Context, entity interface{}) error
RepoFunc func(table string) interface{}
CreateQBFunc func(table string) *rqlite.QueryBuilder
TxFunc func(ctx context.Context, fn func(tx rqlite.Tx) error) error
}
func (m *mockRQLiteClient) Query(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
if m.QueryFunc != nil {
return m.QueryFunc(ctx, dest, query, args...)
}
return nil
}
func (m *mockRQLiteClient) Exec(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
if m.ExecFunc != nil {
return m.ExecFunc(ctx, query, args...)
}
return nil, nil
}
func (m *mockRQLiteClient) FindBy(ctx context.Context, dest interface{}, table string, criteria map[string]interface{}, opts ...rqlite.FindOption) error {
if m.FindByFunc != nil {
return m.FindByFunc(ctx, dest, table, criteria, opts...)
}
return nil
}
func (m *mockRQLiteClient) FindOneBy(ctx context.Context, dest interface{}, table string, criteria map[string]interface{}, opts ...rqlite.FindOption) error {
if m.FindOneFunc != nil {
return m.FindOneFunc(ctx, dest, table, criteria, opts...)
}
return nil
}
func (m *mockRQLiteClient) Save(ctx context.Context, entity interface{}) error {
if m.SaveFunc != nil {
return m.SaveFunc(ctx, entity)
}
return nil
}
func (m *mockRQLiteClient) Remove(ctx context.Context, entity interface{}) error {
if m.RemoveFunc != nil {
return m.RemoveFunc(ctx, entity)
}
return nil
}
func (m *mockRQLiteClient) Repository(table string) interface{} {
if m.RepoFunc != nil {
return m.RepoFunc(table)
}
return nil
}
func (m *mockRQLiteClient) CreateQueryBuilder(table string) *rqlite.QueryBuilder {
if m.CreateQBFunc != nil {
return m.CreateQBFunc(table)
}
return nil
}
func (m *mockRQLiteClient) Tx(ctx context.Context, fn func(tx rqlite.Tx) error) error {
if m.TxFunc != nil {
return m.TxFunc(ctx, fn)
}
return nil
}
// mockProcessManager implements a mock process manager for testing
type mockProcessManager struct {
StartFunc func(ctx context.Context, deployment *deployments.Deployment, workDir string) error
StopFunc func(ctx context.Context, deployment *deployments.Deployment) error
RestartFunc func(ctx context.Context, deployment *deployments.Deployment) error
StatusFunc func(ctx context.Context, deployment *deployments.Deployment) (string, error)
GetLogsFunc func(ctx context.Context, deployment *deployments.Deployment, lines int, follow bool) ([]byte, error)
}
func (m *mockProcessManager) Start(ctx context.Context, deployment *deployments.Deployment, workDir string) error {
if m.StartFunc != nil {
return m.StartFunc(ctx, deployment, workDir)
}
return nil
}
func (m *mockProcessManager) Stop(ctx context.Context, deployment *deployments.Deployment) error {
if m.StopFunc != nil {
return m.StopFunc(ctx, deployment)
}
return nil
}
func (m *mockProcessManager) Restart(ctx context.Context, deployment *deployments.Deployment) error {
if m.RestartFunc != nil {
return m.RestartFunc(ctx, deployment)
}
return nil
}
func (m *mockProcessManager) Status(ctx context.Context, deployment *deployments.Deployment) (string, error) {
if m.StatusFunc != nil {
return m.StatusFunc(ctx, deployment)
}
return "active", nil
}
func (m *mockProcessManager) GetLogs(ctx context.Context, deployment *deployments.Deployment, lines int, follow bool) ([]byte, error) {
if m.GetLogsFunc != nil {
return m.GetLogsFunc(ctx, deployment, lines, follow)
}
return []byte("mock logs"), nil
}
// mockHomeNodeManager implements a mock home node manager for testing
type mockHomeNodeManager struct {
AssignHomeNodeFunc func(ctx context.Context, namespace string) (string, error)
GetHomeNodeFunc func(ctx context.Context, namespace string) (string, error)
}
func (m *mockHomeNodeManager) AssignHomeNode(ctx context.Context, namespace string) (string, error) {
if m.AssignHomeNodeFunc != nil {
return m.AssignHomeNodeFunc(ctx, namespace)
}
return "node-test123", nil
}
func (m *mockHomeNodeManager) GetHomeNode(ctx context.Context, namespace string) (string, error) {
if m.GetHomeNodeFunc != nil {
return m.GetHomeNodeFunc(ctx, namespace)
}
return "node-test123", nil
}
// mockPortAllocator implements a mock port allocator for testing
type mockPortAllocator struct {
AllocatePortFunc func(ctx context.Context, nodeID, deploymentID string) (int, error)
ReleasePortFunc func(ctx context.Context, nodeID string, port int) error
}
func (m *mockPortAllocator) AllocatePort(ctx context.Context, nodeID, deploymentID string) (int, error) {
if m.AllocatePortFunc != nil {
return m.AllocatePortFunc(ctx, nodeID, deploymentID)
}
return 10100, nil
}
func (m *mockPortAllocator) ReleasePort(ctx context.Context, nodeID string, port int) error {
if m.ReleasePortFunc != nil {
return m.ReleasePortFunc(ctx, nodeID, port)
}
return nil
}

View File

@ -179,14 +179,14 @@ func (s *DeploymentService) CreateDNSRecords(ctx context.Context, deployment *de
}
// Create node-specific record
nodeFQDN := fmt.Sprintf("%s.%s.debros.network.", deployment.Name, deployment.HomeNodeID)
nodeFQDN := fmt.Sprintf("%s.%s.orama.network.", deployment.Name, deployment.HomeNodeID)
if err := s.createDNSRecord(ctx, nodeFQDN, "A", nodeIP, deployment.Namespace, deployment.ID); err != nil {
s.logger.Error("Failed to create node-specific DNS record", zap.Error(err))
}
// Create load-balanced record if subdomain is set
if deployment.Subdomain != "" {
lbFQDN := fmt.Sprintf("%s.debros.network.", deployment.Subdomain)
lbFQDN := fmt.Sprintf("%s.orama.network.", deployment.Subdomain)
if err := s.createDNSRecord(ctx, lbFQDN, "A", nodeIP, deployment.Namespace, deployment.ID); err != nil {
s.logger.Error("Failed to create load-balanced DNS record", zap.Error(err))
}
@ -231,11 +231,11 @@ func (s *DeploymentService) getNodeIP(ctx context.Context, nodeID string) (strin
// BuildDeploymentURLs builds all URLs for a deployment
func (s *DeploymentService) BuildDeploymentURLs(deployment *deployments.Deployment) []string {
urls := []string{
fmt.Sprintf("https://%s.%s.debros.network", deployment.Name, deployment.HomeNodeID),
fmt.Sprintf("https://%s.%s.orama.network", deployment.Name, deployment.HomeNodeID),
}
if deployment.Subdomain != "" {
urls = append(urls, fmt.Sprintf("https://%s.debros.network", deployment.Subdomain))
urls = append(urls, fmt.Sprintf("https://%s.orama.network", deployment.Subdomain))
}
return urls

View File

@ -0,0 +1,522 @@
package sqlite
import (
"bytes"
"context"
"database/sql"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"github.com/DeBrosOfficial/network/pkg/deployments"
"github.com/DeBrosOfficial/network/pkg/ipfs"
"github.com/DeBrosOfficial/network/pkg/rqlite"
"go.uber.org/zap"
)
// Mock implementations
type mockRQLiteClient struct {
QueryFunc func(ctx context.Context, dest interface{}, query string, args ...interface{}) error
ExecFunc func(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
FindByFunc func(ctx context.Context, dest interface{}, table string, criteria map[string]interface{}, opts ...rqlite.FindOption) error
FindOneFunc func(ctx context.Context, dest interface{}, table string, criteria map[string]interface{}, opts ...rqlite.FindOption) error
SaveFunc func(ctx context.Context, entity interface{}) error
RemoveFunc func(ctx context.Context, entity interface{}) error
RepoFunc func(table string) interface{}
CreateQBFunc func(table string) *rqlite.QueryBuilder
TxFunc func(ctx context.Context, fn func(tx rqlite.Tx) error) error
}
func (m *mockRQLiteClient) Query(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
if m.QueryFunc != nil {
return m.QueryFunc(ctx, dest, query, args...)
}
return nil
}
func (m *mockRQLiteClient) Exec(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
if m.ExecFunc != nil {
return m.ExecFunc(ctx, query, args...)
}
return nil, nil
}
func (m *mockRQLiteClient) FindBy(ctx context.Context, dest interface{}, table string, criteria map[string]interface{}, opts ...rqlite.FindOption) error {
if m.FindByFunc != nil {
return m.FindByFunc(ctx, dest, table, criteria, opts...)
}
return nil
}
func (m *mockRQLiteClient) FindOneBy(ctx context.Context, dest interface{}, table string, criteria map[string]interface{}, opts ...rqlite.FindOption) error {
if m.FindOneFunc != nil {
return m.FindOneFunc(ctx, dest, table, criteria, opts...)
}
return nil
}
func (m *mockRQLiteClient) Save(ctx context.Context, entity interface{}) error {
if m.SaveFunc != nil {
return m.SaveFunc(ctx, entity)
}
return nil
}
func (m *mockRQLiteClient) Remove(ctx context.Context, entity interface{}) error {
if m.RemoveFunc != nil {
return m.RemoveFunc(ctx, entity)
}
return nil
}
func (m *mockRQLiteClient) Repository(table string) interface{} {
if m.RepoFunc != nil {
return m.RepoFunc(table)
}
return nil
}
func (m *mockRQLiteClient) CreateQueryBuilder(table string) *rqlite.QueryBuilder {
if m.CreateQBFunc != nil {
return m.CreateQBFunc(table)
}
return nil
}
func (m *mockRQLiteClient) Tx(ctx context.Context, fn func(tx rqlite.Tx) error) error {
if m.TxFunc != nil {
return m.TxFunc(ctx, fn)
}
return nil
}
type mockIPFSClient struct {
AddFunc func(ctx context.Context, r io.Reader, filename string) (*ipfs.AddResponse, error)
GetFunc func(ctx context.Context, path, ipfsAPIURL string) (io.ReadCloser, error)
PinFunc func(ctx context.Context, cid, name string, replicationFactor int) (*ipfs.PinResponse, error)
PinStatusFunc func(ctx context.Context, cid string) (*ipfs.PinStatus, error)
UnpinFunc func(ctx context.Context, cid string) error
HealthFunc func(ctx context.Context) error
GetPeerFunc func(ctx context.Context) (int, error)
CloseFunc func(ctx context.Context) error
}
func (m *mockIPFSClient) Add(ctx context.Context, r io.Reader, filename string) (*ipfs.AddResponse, error) {
if m.AddFunc != nil {
return m.AddFunc(ctx, r, filename)
}
return &ipfs.AddResponse{Cid: "QmTestCID123456789"}, nil
}
func (m *mockIPFSClient) Get(ctx context.Context, cid, ipfsAPIURL string) (io.ReadCloser, error) {
if m.GetFunc != nil {
return m.GetFunc(ctx, cid, ipfsAPIURL)
}
return io.NopCloser(nil), nil
}
func (m *mockIPFSClient) Pin(ctx context.Context, cid, name string, replicationFactor int) (*ipfs.PinResponse, error) {
if m.PinFunc != nil {
return m.PinFunc(ctx, cid, name, replicationFactor)
}
return &ipfs.PinResponse{}, nil
}
func (m *mockIPFSClient) PinStatus(ctx context.Context, cid string) (*ipfs.PinStatus, error) {
if m.PinStatusFunc != nil {
return m.PinStatusFunc(ctx, cid)
}
return &ipfs.PinStatus{}, nil
}
func (m *mockIPFSClient) Unpin(ctx context.Context, cid string) error {
if m.UnpinFunc != nil {
return m.UnpinFunc(ctx, cid)
}
return nil
}
func (m *mockIPFSClient) Health(ctx context.Context) error {
if m.HealthFunc != nil {
return m.HealthFunc(ctx)
}
return nil
}
func (m *mockIPFSClient) GetPeerCount(ctx context.Context) (int, error) {
if m.GetPeerFunc != nil {
return m.GetPeerFunc(ctx)
}
return 5, nil
}
func (m *mockIPFSClient) Close(ctx context.Context) error {
if m.CloseFunc != nil {
return m.CloseFunc(ctx)
}
return nil
}
// TestCreateDatabase_Success tests creating a new database
func TestCreateDatabase_Success(t *testing.T) {
mockDB := &mockRQLiteClient{
QueryFunc: func(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
// For dns_nodes query, return mock active node
if strings.Contains(query, "dns_nodes") {
destValue := reflect.ValueOf(dest)
if destValue.Kind() == reflect.Ptr {
sliceValue := destValue.Elem()
if sliceValue.Kind() == reflect.Slice {
elemType := sliceValue.Type().Elem()
newElem := reflect.New(elemType).Elem()
idField := newElem.FieldByName("ID")
if idField.IsValid() && idField.CanSet() {
idField.SetString("node-test123")
}
sliceValue.Set(reflect.Append(sliceValue, newElem))
}
}
}
// For database check, return empty (database doesn't exist)
if strings.Contains(query, "namespace_sqlite_databases") && strings.Contains(query, "SELECT") {
// Return empty result
}
return nil
},
ExecFunc: func(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
return nil, nil
},
}
// Create temp directory for test database
tmpDir := t.TempDir()
portAlloc := deployments.NewPortAllocator(mockDB, zap.NewNop())
homeNodeMgr := deployments.NewHomeNodeManager(mockDB, portAlloc, zap.NewNop())
handler := NewSQLiteHandler(mockDB, homeNodeMgr, zap.NewNop())
handler.basePath = tmpDir
reqBody := map[string]string{
"database_name": "test-db",
}
bodyBytes, _ := json.Marshal(reqBody)
req := httptest.NewRequest("POST", "/v1/db/sqlite/create", bytes.NewReader(bodyBytes))
ctx := context.WithValue(req.Context(), "namespace", "test-namespace")
req = req.WithContext(ctx)
rr := httptest.NewRecorder()
handler.CreateDatabase(rr, req)
if rr.Code != http.StatusCreated {
t.Errorf("Expected status 201, got %d", rr.Code)
t.Logf("Response: %s", rr.Body.String())
}
// Verify database file was created
dbPath := filepath.Join(tmpDir, "test-namespace", "test-db.db")
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
t.Errorf("Expected database file to be created at %s", dbPath)
}
// Verify response
var resp map[string]interface{}
json.NewDecoder(rr.Body).Decode(&resp)
if resp["database_name"] != "test-db" {
t.Errorf("Expected database_name 'test-db', got %v", resp["database_name"])
}
}
// TestCreateDatabase_DuplicateName tests that duplicate database names are rejected
func TestCreateDatabase_DuplicateName(t *testing.T) {
mockDB := &mockRQLiteClient{
QueryFunc: func(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
// For dns_nodes query
if strings.Contains(query, "dns_nodes") {
destValue := reflect.ValueOf(dest)
if destValue.Kind() == reflect.Ptr {
sliceValue := destValue.Elem()
if sliceValue.Kind() == reflect.Slice {
elemType := sliceValue.Type().Elem()
newElem := reflect.New(elemType).Elem()
idField := newElem.FieldByName("ID")
if idField.IsValid() && idField.CanSet() {
idField.SetString("node-test123")
}
sliceValue.Set(reflect.Append(sliceValue, newElem))
}
}
}
// For database check, return existing database
if strings.Contains(query, "namespace_sqlite_databases") && strings.Contains(query, "SELECT") {
destValue := reflect.ValueOf(dest)
if destValue.Kind() == reflect.Ptr {
sliceValue := destValue.Elem()
if sliceValue.Kind() == reflect.Slice {
elemType := sliceValue.Type().Elem()
newElem := reflect.New(elemType).Elem()
// Set ID field to indicate existing database
idField := newElem.FieldByName("ID")
if idField.IsValid() && idField.CanSet() {
idField.SetString("existing-db-id")
}
sliceValue.Set(reflect.Append(sliceValue, newElem))
}
}
}
return nil
},
}
tmpDir := t.TempDir()
portAlloc := deployments.NewPortAllocator(mockDB, zap.NewNop())
homeNodeMgr := deployments.NewHomeNodeManager(mockDB, portAlloc, zap.NewNop())
handler := NewSQLiteHandler(mockDB, homeNodeMgr, zap.NewNop())
handler.basePath = tmpDir
reqBody := map[string]string{
"database_name": "test-db",
}
bodyBytes, _ := json.Marshal(reqBody)
req := httptest.NewRequest("POST", "/v1/db/sqlite/create", bytes.NewReader(bodyBytes))
ctx := context.WithValue(req.Context(), "namespace", "test-namespace")
req = req.WithContext(ctx)
rr := httptest.NewRecorder()
handler.CreateDatabase(rr, req)
if rr.Code != http.StatusConflict {
t.Errorf("Expected status 409 (Conflict), got %d", rr.Code)
}
}
// TestCreateDatabase_InvalidName tests that invalid database names are rejected
func TestCreateDatabase_InvalidName(t *testing.T) {
mockDB := &mockRQLiteClient{}
tmpDir := t.TempDir()
portAlloc := deployments.NewPortAllocator(mockDB, zap.NewNop())
homeNodeMgr := deployments.NewHomeNodeManager(mockDB, portAlloc, zap.NewNop())
handler := NewSQLiteHandler(mockDB, homeNodeMgr, zap.NewNop())
handler.basePath = tmpDir
invalidNames := []string{
"test db", // Space
"test@db", // Special char
"test/db", // Slash
"", // Empty
strings.Repeat("a", 100), // Too long
}
for _, name := range invalidNames {
reqBody := map[string]string{
"database_name": name,
}
bodyBytes, _ := json.Marshal(reqBody)
req := httptest.NewRequest("POST", "/v1/db/sqlite/create", bytes.NewReader(bodyBytes))
ctx := context.WithValue(req.Context(), "namespace", "test-namespace")
req = req.WithContext(ctx)
rr := httptest.NewRecorder()
handler.CreateDatabase(rr, req)
if rr.Code != http.StatusBadRequest {
t.Errorf("Expected status 400 for invalid name %q, got %d", name, rr.Code)
}
}
}
// TestListDatabases tests listing all databases for a namespace
func TestListDatabases(t *testing.T) {
mockDB := &mockRQLiteClient{
QueryFunc: func(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
// Return empty list
return nil
},
}
portAlloc := deployments.NewPortAllocator(mockDB, zap.NewNop())
homeNodeMgr := deployments.NewHomeNodeManager(mockDB, portAlloc, zap.NewNop())
handler := NewSQLiteHandler(mockDB, homeNodeMgr, zap.NewNop())
req := httptest.NewRequest("GET", "/v1/db/sqlite/list", nil)
ctx := context.WithValue(req.Context(), "namespace", "test-namespace")
req = req.WithContext(ctx)
rr := httptest.NewRecorder()
handler.ListDatabases(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", rr.Code)
}
var resp map[string]interface{}
json.NewDecoder(rr.Body).Decode(&resp)
if _, ok := resp["databases"]; !ok {
t.Error("Expected 'databases' field in response")
}
if _, ok := resp["count"]; !ok {
t.Error("Expected 'count' field in response")
}
}
// TestBackupDatabase tests backing up a database to IPFS
func TestBackupDatabase(t *testing.T) {
// Create a temporary database file
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "test.db")
// Create a real SQLite database
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
t.Fatalf("Failed to create test database: %v", err)
}
db.Exec("CREATE TABLE test (id INTEGER PRIMARY KEY)")
db.Close()
mockDB := &mockRQLiteClient{
QueryFunc: func(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
// Mock database record lookup - return struct with file_path
if strings.Contains(query, "namespace_sqlite_databases") {
destValue := reflect.ValueOf(dest)
if destValue.Kind() == reflect.Ptr {
sliceValue := destValue.Elem()
if sliceValue.Kind() == reflect.Slice {
elemType := sliceValue.Type().Elem()
newElem := reflect.New(elemType).Elem()
// Set fields
idField := newElem.FieldByName("ID")
if idField.IsValid() && idField.CanSet() {
idField.SetString("test-db-id")
}
filePathField := newElem.FieldByName("FilePath")
if filePathField.IsValid() && filePathField.CanSet() {
filePathField.SetString(dbPath)
}
sliceValue.Set(reflect.Append(sliceValue, newElem))
}
}
}
return nil
},
ExecFunc: func(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
return nil, nil
},
}
mockIPFS := &mockIPFSClient{
AddFunc: func(ctx context.Context, r io.Reader, filename string) (*ipfs.AddResponse, error) {
// Verify data is being uploaded
data, _ := io.ReadAll(r)
if len(data) == 0 {
t.Error("Expected non-empty database file upload")
}
return &ipfs.AddResponse{Cid: "QmBackupCID123"}, nil
},
}
portAlloc := deployments.NewPortAllocator(mockDB, zap.NewNop())
homeNodeMgr := deployments.NewHomeNodeManager(mockDB, portAlloc, zap.NewNop())
sqliteHandler := NewSQLiteHandler(mockDB, homeNodeMgr, zap.NewNop())
backupHandler := NewBackupHandler(sqliteHandler, mockIPFS, zap.NewNop())
reqBody := map[string]string{
"database_name": "test-db",
}
bodyBytes, _ := json.Marshal(reqBody)
req := httptest.NewRequest("POST", "/v1/db/sqlite/backup", bytes.NewReader(bodyBytes))
ctx := context.WithValue(req.Context(), "namespace", "test-namespace")
req = req.WithContext(ctx)
rr := httptest.NewRecorder()
backupHandler.BackupDatabase(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", rr.Code)
t.Logf("Response: %s", rr.Body.String())
}
var resp map[string]interface{}
json.NewDecoder(rr.Body).Decode(&resp)
if resp["backup_cid"] != "QmBackupCID123" {
t.Errorf("Expected backup_cid 'QmBackupCID123', got %v", resp["backup_cid"])
}
}
// TestIsValidDatabaseName tests database name validation
func TestIsValidDatabaseName(t *testing.T) {
tests := []struct {
name string
valid bool
}{
{"valid_db", true},
{"valid-db", true},
{"ValidDB123", true},
{"test_db_123", true},
{"test db", false}, // Space
{"test@db", false}, // Special char
{"test/db", false}, // Slash
{"", false}, // Empty
{strings.Repeat("a", 65), false}, // Too long
}
for _, tt := range tests {
result := isValidDatabaseName(tt.name)
if result != tt.valid {
t.Errorf("isValidDatabaseName(%q) = %v, expected %v", tt.name, result, tt.valid)
}
}
}
// TestIsWriteQuery tests SQL query classification
func TestIsWriteQuery(t *testing.T) {
tests := []struct {
query string
isWrite bool
}{
{"SELECT * FROM users", false},
{"INSERT INTO users VALUES (1, 'test')", true},
{"UPDATE users SET name = 'test'", true},
{"DELETE FROM users WHERE id = 1", true},
{"CREATE TABLE test (id INT)", true},
{"DROP TABLE test", true},
{"ALTER TABLE test ADD COLUMN name TEXT", true},
{" insert into users values (1)", true}, // Case insensitive with whitespace
{"select * from users", false},
}
for _, tt := range tests {
result := isWriteQuery(tt.query)
if result != tt.isWrite {
t.Errorf("isWriteQuery(%q) = %v, expected %v", tt.query, result, tt.isWrite)
}
}
}

View File

@ -439,8 +439,8 @@ func (g *Gateway) domainRoutingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
host := strings.Split(r.Host, ":")[0] // Strip port
// Only process .debros.network domains
if !strings.HasSuffix(host, ".debros.network") {
// Only process .orama.network domains
if !strings.HasSuffix(host, ".orama.network") {
next.ServeHTTP(w, r)
return
}
@ -497,7 +497,7 @@ func (g *Gateway) getDeploymentByDomain(ctx context.Context, domain string) (*de
SELECT d.id, d.namespace, d.name, d.type, d.port, d.content_cid, d.status
FROM deployments d
LEFT JOIN deployment_domains dd ON d.id = dd.deployment_id
WHERE (d.name || '.node-' || d.home_node_id || '.debros.network' = ?
WHERE (d.name || '.node-' || d.home_node_id || '.orama.network' = ?
OR dd.domain = ? AND dd.verification_status = 'verified')
AND d.status = 'active'
LIMIT 1

View File

@ -26,3 +26,110 @@ func TestExtractAPIKey(t *testing.T) {
t.Fatalf("got %q", got)
}
}
// TestDomainRoutingMiddleware_NonDebrosNetwork tests that non-debros domains pass through
func TestDomainRoutingMiddleware_NonDebrosNetwork(t *testing.T) {
nextCalled := false
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
nextCalled = true
w.WriteHeader(http.StatusOK)
})
g := &Gateway{}
middleware := g.domainRoutingMiddleware(next)
req := httptest.NewRequest("GET", "/", nil)
req.Host = "example.com"
rr := httptest.NewRecorder()
middleware.ServeHTTP(rr, req)
if !nextCalled {
t.Error("Expected next handler to be called for non-debros domain")
}
if rr.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", rr.Code)
}
}
// TestDomainRoutingMiddleware_APIPathBypass tests that /v1/ paths bypass routing
func TestDomainRoutingMiddleware_APIPathBypass(t *testing.T) {
nextCalled := false
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
nextCalled = true
w.WriteHeader(http.StatusOK)
})
g := &Gateway{}
middleware := g.domainRoutingMiddleware(next)
req := httptest.NewRequest("GET", "/v1/deployments/list", nil)
req.Host = "myapp.orama.network"
rr := httptest.NewRecorder()
middleware.ServeHTTP(rr, req)
if !nextCalled {
t.Error("Expected next handler to be called for /v1/ path")
}
if rr.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", rr.Code)
}
}
// TestDomainRoutingMiddleware_WellKnownBypass tests that /.well-known/ paths bypass routing
func TestDomainRoutingMiddleware_WellKnownBypass(t *testing.T) {
nextCalled := false
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
nextCalled = true
w.WriteHeader(http.StatusOK)
})
g := &Gateway{}
middleware := g.domainRoutingMiddleware(next)
req := httptest.NewRequest("GET", "/.well-known/acme-challenge/test", nil)
req.Host = "myapp.orama.network"
rr := httptest.NewRecorder()
middleware.ServeHTTP(rr, req)
if !nextCalled {
t.Error("Expected next handler to be called for /.well-known/ path")
}
if rr.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", rr.Code)
}
}
// TestDomainRoutingMiddleware_NoDeploymentService tests graceful handling when deployment service is nil
func TestDomainRoutingMiddleware_NoDeploymentService(t *testing.T) {
nextCalled := false
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
nextCalled = true
w.WriteHeader(http.StatusOK)
})
g := &Gateway{
// deploymentService is nil
staticHandler: nil,
}
middleware := g.domainRoutingMiddleware(next)
req := httptest.NewRequest("GET", "/", nil)
req.Host = "myapp.orama.network"
rr := httptest.NewRecorder()
middleware.ServeHTTP(rr, req)
if !nextCalled {
t.Error("Expected next handler to be called when deployment service is nil")
}
if rr.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", rr.Code)
}
}

View File

@ -14,13 +14,13 @@ var (
// Global cache of trusted domains loaded from environment
trustedDomains []string
// CA certificate pool for trusting self-signed certs
caCertPool *x509.CertPool
initialized bool
caCertPool *x509.CertPool
initialized bool
)
// Default trusted domains - always trust debros.network for staging/development
// Default trusted domains - always trust orama.network for staging/development
var defaultTrustedDomains = []string{
"*.debros.network",
"*.orama.network",
}
// init loads trusted domains and CA certificate from environment and files
@ -64,7 +64,7 @@ func GetTrustedDomains() []string {
func ShouldSkipTLSVerify(domain string) bool {
for _, trusted := range trustedDomains {
if strings.HasPrefix(trusted, "*.") {
// Handle wildcards like *.debros.network
// Handle wildcards like *.orama.network
suffix := strings.TrimPrefix(trusted, "*")
if strings.HasSuffix(domain, suffix) || domain == strings.TrimPrefix(suffix, ".") {
return true
@ -119,4 +119,3 @@ func NewHTTPClientForDomain(timeout time.Duration, hostname string) *http.Client
},
}
}

View File

@ -32,7 +32,7 @@ for i in "${!NODES[@]}"; do
node="${NODES[$i]}"
node_num=$((i + 1))
echo "[$node_num/4] Deploying to ns${node_num}.debros.network ($node)..."
echo "[$node_num/4] Deploying to ns${node_num}.orama.network ($node)..."
# Copy binary
echo " → Copying binary..."
@ -59,9 +59,9 @@ for i in "${!NODES[@]}"; do
# Check status
echo " → Checking status..."
if ssh "debros@$node" "sudo systemctl is-active --quiet coredns"; then
echo " ✅ CoreDNS running on ns${node_num}.debros.network"
echo " ✅ CoreDNS running on ns${node_num}.orama.network"
else
echo " ❌ CoreDNS failed to start on ns${node_num}.debros.network"
echo " ❌ CoreDNS failed to start on ns${node_num}.orama.network"
echo " Check logs: ssh debros@$node sudo journalctl -u coredns -n 50"
fi
@ -71,14 +71,14 @@ done
echo "✅ Deployment complete!"
echo ""
echo "Next steps:"
echo " 1. Test DNS resolution: dig @${NODES[0]} test.debros.network"
echo " 1. Test DNS resolution: dig @${NODES[0]} test.orama.network"
echo " 2. Update registrar NS records (ONLY after testing):"
echo " NS debros.network. ns1.debros.network."
echo " NS debros.network. ns2.debros.network."
echo " NS debros.network. ns3.debros.network."
echo " NS debros.network. ns4.debros.network."
echo " A ns1.debros.network. ${NODES[0]}"
echo " A ns2.debros.network. ${NODES[1]}"
echo " A ns3.debros.network. ${NODES[2]}"
echo " A ns4.debros.network. ${NODES[3]}"
echo " NS orama.network. ns1.orama.network."
echo " NS orama.network. ns2.orama.network."
echo " NS orama.network. ns3.orama.network."
echo " NS orama.network. ns4.orama.network."
echo " A ns1.orama.network. ${NODES[0]}"
echo " A ns2.orama.network. ${NODES[1]}"
echo " A ns3.orama.network. ${NODES[2]}"
echo " A ns4.orama.network. ${NODES[3]}"
echo ""

View File

@ -124,7 +124,7 @@ log_info " sudo systemctl status coredns"
log_info " sudo journalctl -u coredns -f"
echo
log_info "To test DNS:"
log_info " dig @localhost test.debros.network"
log_info " dig @localhost test.orama.network"
# Cleanup
rm -f /tmp/coredns.tgz

15
testdata/.gitignore vendored Normal file
View File

@ -0,0 +1,15 @@
# Dependencies
apps/*/node_modules/
apps/*/.next/
apps/*/dist/
# Build outputs
apps/go-backend/api
tarballs/*.tar.gz
# Logs
*.log
npm-debug.log*
# OS files
.DS_Store

138
testdata/README.md vendored Normal file
View File

@ -0,0 +1,138 @@
# E2E Test Fixtures
This directory contains test applications used for end-to-end testing of the Orama Network deployment system.
## Test Applications
### 1. React Vite App (`apps/react-vite/`)
A minimal React application built with Vite for testing static site deployments.
**Features:**
- Simple counter component
- CSS and JavaScript assets
- Test markers for E2E validation
**Build:**
```bash
cd apps/react-vite
npm install
npm run build
# Output: dist/
```
### 2. Next.js SSR App (`apps/nextjs-ssr/`)
A Next.js application with server-side rendering and API routes for testing dynamic deployments.
**Features:**
- Server-side rendered homepage
- API routes:
- `/api/hello` - Simple greeting endpoint
- `/api/data` - JSON data with users list
- TypeScript support
**Build:**
```bash
cd apps/nextjs-ssr
npm install
npm run build
# Output: .next/
```
### 3. Go Backend (`apps/go-backend/`)
A simple Go HTTP API for testing native backend deployments.
**Features:**
- Health check endpoint: `/health`
- Users API: `/api/users` (GET, POST)
- Environment variable support (PORT)
**Build:**
```bash
cd apps/go-backend
make build
# Output: api (Linux binary)
```
## Building All Fixtures
Use the build script to create deployment-ready tarballs for all test apps:
```bash
./build-fixtures.sh
```
This will:
1. Build all three applications
2. Create compressed tarballs in `tarballs/`:
- `react-vite.tar.gz` - Static site deployment
- `nextjs-ssr.tar.gz` - Next.js SSR deployment
- `go-backend.tar.gz` - Go backend deployment
## Tarballs
Pre-built deployment artifacts are stored in `tarballs/` for use in E2E tests.
**Usage in tests:**
```go
tarballPath := filepath.Join("../../testdata/tarballs/react-vite.tar.gz")
file, err := os.Open(tarballPath)
// Upload to deployment endpoint
```
## Directory Structure
```
testdata/
├── apps/ # Source applications
│ ├── react-vite/ # React + Vite static app
│ ├── nextjs-ssr/ # Next.js SSR app
│ └── go-backend/ # Go HTTP API
├── tarballs/ # Deployment artifacts
│ ├── react-vite.tar.gz
│ ├── nextjs-ssr.tar.gz
│ └── go-backend.tar.gz
├── build-fixtures.sh # Build script
└── README.md # This file
```
## Development
To modify test apps:
1. Edit source files in `apps/{app-name}/`
2. Run `./build-fixtures.sh` to rebuild
3. Tarballs are automatically updated for E2E tests
## Testing Locally
### React Vite App
```bash
cd apps/react-vite
npm run dev
# Open http://localhost:5173
```
### Next.js App
```bash
cd apps/nextjs-ssr
npm run dev
# Open http://localhost:3000
# Test API: http://localhost:3000/api/hello
```
### Go Backend
```bash
cd apps/go-backend
go run main.go
# Test: curl http://localhost:8080/health
# Test: curl http://localhost:8080/api/users
```
## Notes
- All apps are intentionally minimal to ensure fast build and deployment times
- React and Next.js apps include test markers (`data-testid`) for E2E validation
- Go backend uses standard library only (no external dependencies)
- Build script requires: Node.js (18+), npm, Go (1.21+), tar, gzip

10
testdata/apps/go-backend/Makefile vendored Normal file
View File

@ -0,0 +1,10 @@
.PHONY: build clean run
build:
GOOS=linux GOARCH=amd64 go build -o api main.go
clean:
rm -f api
run:
go run main.go

3
testdata/apps/go-backend/go.mod vendored Normal file
View File

@ -0,0 +1,3 @@
module github.com/DeBrosOfficial/network/testdata/apps/go-backend
go 1.21

109
testdata/apps/go-backend/main.go vendored Normal file
View File

@ -0,0 +1,109 @@
package main
import (
"encoding/json"
"log"
"net/http"
"os"
"time"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
}
type HealthResponse struct {
Status string `json:"status"`
Timestamp time.Time `json:"timestamp"`
Service string `json:"service"`
}
type UsersResponse struct {
Users []User `json:"users"`
Total int `json:"total"`
}
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}
var users = []User{
{ID: 1, Name: "Alice", Email: "alice@example.com", CreatedAt: time.Now()},
{ID: 2, Name: "Bob", Email: "bob@example.com", CreatedAt: time.Now()},
{ID: 3, Name: "Charlie", Email: "charlie@example.com", CreatedAt: time.Now()},
}
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(HealthResponse{
Status: "healthy",
Timestamp: time.Now(),
Service: "go-backend-test",
})
}
func usersHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.Method {
case http.MethodGet:
json.NewEncoder(w).Encode(UsersResponse{
Users: users,
Total: len(users),
})
case http.MethodPost:
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
newUser := User{
ID: len(users) + 1,
Name: req.Name,
Email: req.Email,
CreatedAt: time.Now(),
}
users = append(users, newUser)
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"user": newUser,
})
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func rootHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"message": "Orama Network Go Backend Test",
"version": "1.0.0",
"endpoints": map[string]string{
"health": "/health",
"users": "/api/users",
},
})
}
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
http.HandleFunc("/", rootHandler)
http.HandleFunc("/health", healthHandler)
http.HandleFunc("/api/users", usersHandler)
log.Printf("Starting Go backend on port %s", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}

View File

@ -0,0 +1,26 @@
import { NextResponse } from 'next/server'
export async function GET() {
return NextResponse.json({
users: [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
{ id: 3, name: 'Charlie', email: 'charlie@example.com' }
],
total: 3,
timestamp: new Date().toISOString()
})
}
export async function POST(request: Request) {
const body = await request.json()
return NextResponse.json({
success: true,
created: {
id: 4,
...body,
createdAt: new Date().toISOString()
}
}, { status: 201 })
}

View File

@ -0,0 +1,9 @@
import { NextResponse } from 'next/server'
export async function GET() {
return NextResponse.json({
message: 'Hello from Orama Network!',
timestamp: new Date().toISOString(),
service: 'nextjs-ssr-test'
})
}

View File

@ -0,0 +1,72 @@
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
h1 {
font-size: 3rem;
color: #333;
margin-bottom: 1rem;
}
.test-marker {
font-size: 1.2rem;
color: #666;
margin-bottom: 2rem;
}
.card {
background: white;
border-radius: 8px;
padding: 2rem;
margin: 2rem 0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.api-test {
background: #f9f9f9;
border-radius: 8px;
padding: 1.5rem;
margin: 2rem 0;
}
.api-test ul {
list-style: none;
padding-left: 0;
}
.api-test li {
margin: 0.5rem 0;
}
.api-test a {
color: #0070f3;
text-decoration: none;
}
.api-test a:hover {
text-decoration: underline;
}
.version {
margin-top: 2rem;
color: #999;
font-size: 0.9rem;
}

19
testdata/apps/nextjs-ssr/app/layout.tsx vendored Normal file
View File

@ -0,0 +1,19 @@
import type { Metadata } from 'next'
import './globals.css'
export const metadata: Metadata = {
title: 'Orama Network Next.js Test',
description: 'E2E testing for Next.js SSR deployments',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}

23
testdata/apps/nextjs-ssr/app/page.tsx vendored Normal file
View File

@ -0,0 +1,23 @@
export default function Home() {
return (
<main className="container">
<h1>Orama Network Next.js Test</h1>
<p className="test-marker" data-testid="app-title">
E2E Testing - Next.js SSR Deployment
</p>
<div className="card">
<h2>Server-Side Rendering Test</h2>
<p>This page is rendered on the server.</p>
<p>Current time: {new Date().toISOString()}</p>
</div>
<div className="api-test">
<h3>API Routes:</h3>
<ul>
<li><a href="/api/hello">/api/hello</a> - Simple greeting endpoint</li>
<li><a href="/api/data">/api/data</a> - JSON data endpoint</li>
</ul>
</div>
<p className="version">Version: 1.0.0</p>
</main>
)
}

View File

@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.

View File

@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
}
module.exports = nextConfig

501
testdata/apps/nextjs-ssr/package-lock.json generated vendored Normal file
View File

@ -0,0 +1,501 @@
{
"name": "nextjs-ssr-test",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "nextjs-ssr-test",
"version": "1.0.0",
"dependencies": {
"next": "^14.0.4",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"typescript": "^5"
}
},
"node_modules/@next/env": {
"version": "14.2.35",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.35.tgz",
"integrity": "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==",
"license": "MIT"
},
"node_modules/@next/swc-darwin-arm64": {
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz",
"integrity": "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz",
"integrity": "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz",
"integrity": "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz",
"integrity": "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz",
"integrity": "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz",
"integrity": "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz",
"integrity": "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz",
"integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz",
"integrity": "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
"license": "Apache-2.0"
},
"node_modules/@swc/helpers": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz",
"integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==",
"license": "Apache-2.0",
"dependencies": {
"@swc/counter": "^0.1.3",
"tslib": "^2.4.0"
}
},
"node_modules/@types/node": {
"version": "20.19.30",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz",
"integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/prop-types": {
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.27",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
}
},
"node_modules/@types/react-dom": {
"version": "18.3.7",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^18.0.0"
}
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dependencies": {
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=10.16.0"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001765",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz",
"integrity": "sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "CC-BY-4.0"
},
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"dev": true,
"license": "MIT"
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"license": "MIT"
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/next": {
"version": "14.2.35",
"resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz",
"integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==",
"license": "MIT",
"dependencies": {
"@next/env": "14.2.35",
"@swc/helpers": "0.5.5",
"busboy": "1.6.0",
"caniuse-lite": "^1.0.30001579",
"graceful-fs": "^4.2.11",
"postcss": "8.4.31",
"styled-jsx": "5.1.1"
},
"bin": {
"next": "dist/bin/next"
},
"engines": {
"node": ">=18.17.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "14.2.33",
"@next/swc-darwin-x64": "14.2.33",
"@next/swc-linux-arm64-gnu": "14.2.33",
"@next/swc-linux-arm64-musl": "14.2.33",
"@next/swc-linux-x64-gnu": "14.2.33",
"@next/swc-linux-x64-musl": "14.2.33",
"@next/swc-win32-arm64-msvc": "14.2.33",
"@next/swc-win32-ia32-msvc": "14.2.33",
"@next/swc-win32-x64-msvc": "14.2.33"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
"@playwright/test": "^1.41.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sass": "^1.3.0"
},
"peerDependenciesMeta": {
"@opentelemetry/api": {
"optional": true
},
"@playwright/test": {
"optional": true
},
"sass": {
"optional": true
}
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
"node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.6",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
},
"peerDependencies": {
"react": "^18.3.1"
}
},
"node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/styled-jsx": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz",
"integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==",
"license": "MIT",
"dependencies": {
"client-only": "0.0.1"
},
"engines": {
"node": ">= 12.0.0"
},
"peerDependencies": {
"react": ">= 16.8.0 || 17.x.x || ^18.0.0-0"
},
"peerDependenciesMeta": {
"@babel/core": {
"optional": true
},
"babel-plugin-macros": {
"optional": true
}
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
}
}
}

21
testdata/apps/nextjs-ssr/package.json vendored Normal file
View File

@ -0,0 +1,21 @@
{
"name": "nextjs-ssr-test",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "^14.0.4",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"typescript": "^5"
}
}

27
testdata/apps/nextjs-ssr/tsconfig.json vendored Normal file
View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

12
testdata/apps/react-vite/index.html vendored Normal file
View File

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Orama Network Test App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

1716
testdata/apps/react-vite/package-lock.json generated vendored Normal file

File diff suppressed because it is too large Load Diff

21
testdata/apps/react-vite/package.json vendored Normal file
View File

@ -0,0 +1,21 @@
{
"name": "react-vite-test",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@vitejs/plugin-react": "^4.2.1",
"vite": "^5.0.8"
}
}

48
testdata/apps/react-vite/src/App.css vendored Normal file
View File

@ -0,0 +1,48 @@
.app-container {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
color: #646cff;
}
.test-marker {
font-size: 1.2em;
color: #888;
margin: 1rem 0;
}
.card {
padding: 2em;
background-color: #f9f9f9;
border-radius: 8px;
margin: 2rem 0;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
color: white;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
.version {
font-size: 0.9em;
color: #999;
margin-top: 2rem;
}

28
testdata/apps/react-vite/src/App.jsx vendored Normal file
View File

@ -0,0 +1,28 @@
import { useState } from 'react'
import './App.css'
function App() {
const [count, setCount] = useState(0)
return (
<>
<div className="app-container">
<h1>Orama Network Test App</h1>
<p className="test-marker" data-testid="app-title">
E2E Testing - React Vite Static Deployment
</p>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
This is a test application for validating static site deployments.
</p>
</div>
<p className="version">Version: 1.0.0</p>
</div>
</>
)
}
export default App

40
testdata/apps/react-vite/src/index.css vendored Normal file
View File

@ -0,0 +1,40 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
button {
background-color: #f9f9f9;
color: #213547;
}
}

10
testdata/apps/react-vite/src/main.jsx vendored Normal file
View File

@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

10
testdata/apps/react-vite/vite.config.js vendored Normal file
View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true
}
})

58
testdata/build-fixtures.sh vendored Executable file
View File

@ -0,0 +1,58 @@
#!/bin/bash
set -e
echo "🔨 Building E2E test fixtures..."
# Get the directory of this script
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "$SCRIPT_DIR"
# Create tarballs directory
mkdir -p tarballs
# Build React Vite app
echo ""
echo "📦 Building React Vite app..."
cd apps/react-vite
if [ ! -d "node_modules" ]; then
echo " Installing dependencies..."
npm install
fi
echo " Building..."
npm run build
echo " Creating tarball..."
tar -czf "$SCRIPT_DIR/tarballs/react-vite.tar.gz" -C dist .
cd "$SCRIPT_DIR"
# Build Next.js app
echo ""
echo "📦 Building Next.js app..."
cd apps/nextjs-ssr
if [ ! -d "node_modules" ]; then
echo " Installing dependencies..."
npm install
fi
echo " Building..."
npm run build
echo " Creating tarball..."
tar -czf "$SCRIPT_DIR/tarballs/nextjs-ssr.tar.gz" .next/ package.json next.config.js
cd "$SCRIPT_DIR"
# Build Go backend
echo ""
echo "📦 Building Go backend..."
cd apps/go-backend
echo " Building Linux binary..."
make build
echo " Creating tarball..."
tar -czf "$SCRIPT_DIR/tarballs/go-backend.tar.gz" api
make clean
cd "$SCRIPT_DIR"
echo ""
echo "✅ All test fixtures built successfully!"
echo ""
echo "Generated tarballs:"
ls -lh tarballs/
echo ""
echo "Ready for E2E testing!"