mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-03-17 09:36:56 +00:00
added some tests
This commit is contained in:
parent
9fc9bbb8e5
commit
0dcde29f7c
2
.github/workflows/release-apt.yml
vendored
2
.github/workflows/release-apt.yml
vendored
@ -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
760
docs/TESTING_PLAN.md
Normal 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
|
||||
```
|
||||
194
e2e/deployments/static_deployment_test.go
Normal file
194
e2e/deployments/static_deployment_test.go
Normal 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
257
e2e/domain_routing_test.go
Normal 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))
|
||||
})
|
||||
}
|
||||
368
e2e/env.go
368
e2e/env.go
@ -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
|
||||
}
|
||||
|
||||
292
e2e/fullstack_integration_test.go
Normal file
292
e2e/fullstack_integration_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
423
e2e/namespace_isolation_test.go
Normal file
423
e2e/namespace_isolation_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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"`
|
||||
}
|
||||
|
||||
|
||||
398
pkg/gateway/handlers/deployments/handlers_test.go
Normal file
398
pkg/gateway/handlers/deployments/handlers_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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{}{
|
||||
|
||||
239
pkg/gateway/handlers/deployments/mocks_test.go
Normal file
239
pkg/gateway/handlers/deployments/mocks_test.go
Normal 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
|
||||
}
|
||||
@ -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
|
||||
|
||||
522
pkg/gateway/handlers/sqlite/handlers_test.go
Normal file
522
pkg/gateway/handlers/sqlite/handlers_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 ""
|
||||
|
||||
@ -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
15
testdata/.gitignore
vendored
Normal 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
138
testdata/README.md
vendored
Normal 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
10
testdata/apps/go-backend/Makefile
vendored
Normal 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
3
testdata/apps/go-backend/go.mod
vendored
Normal 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
109
testdata/apps/go-backend/main.go
vendored
Normal 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))
|
||||
}
|
||||
26
testdata/apps/nextjs-ssr/app/api/data/route.ts
vendored
Normal file
26
testdata/apps/nextjs-ssr/app/api/data/route.ts
vendored
Normal 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 })
|
||||
}
|
||||
9
testdata/apps/nextjs-ssr/app/api/hello/route.ts
vendored
Normal file
9
testdata/apps/nextjs-ssr/app/api/hello/route.ts
vendored
Normal 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'
|
||||
})
|
||||
}
|
||||
72
testdata/apps/nextjs-ssr/app/globals.css
vendored
Normal file
72
testdata/apps/nextjs-ssr/app/globals.css
vendored
Normal 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
19
testdata/apps/nextjs-ssr/app/layout.tsx
vendored
Normal 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
23
testdata/apps/nextjs-ssr/app/page.tsx
vendored
Normal 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>
|
||||
)
|
||||
}
|
||||
5
testdata/apps/nextjs-ssr/next-env.d.ts
vendored
Normal file
5
testdata/apps/nextjs-ssr/next-env.d.ts
vendored
Normal 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.
|
||||
6
testdata/apps/nextjs-ssr/next.config.js
vendored
Normal file
6
testdata/apps/nextjs-ssr/next.config.js
vendored
Normal 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
501
testdata/apps/nextjs-ssr/package-lock.json
generated
vendored
Normal 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
21
testdata/apps/nextjs-ssr/package.json
vendored
Normal 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
27
testdata/apps/nextjs-ssr/tsconfig.json
vendored
Normal 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
12
testdata/apps/react-vite/index.html
vendored
Normal 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
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
21
testdata/apps/react-vite/package.json
vendored
Normal 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
48
testdata/apps/react-vite/src/App.css
vendored
Normal 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
28
testdata/apps/react-vite/src/App.jsx
vendored
Normal 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
40
testdata/apps/react-vite/src/index.css
vendored
Normal 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
10
testdata/apps/react-vite/src/main.jsx
vendored
Normal 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
10
testdata/apps/react-vite/vite.config.js
vendored
Normal 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
58
testdata/build-fixtures.sh
vendored
Executable 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!"
|
||||
Loading…
x
Reference in New Issue
Block a user