mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-03-17 09:16:57 +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
|
Priority: optional
|
||||||
Architecture: ${ARCH}
|
Architecture: ${ARCH}
|
||||||
Depends: libc6
|
Depends: libc6
|
||||||
Maintainer: DeBros Team <team@debros.network>
|
Maintainer: DeBros Team <team@orama.network>
|
||||||
Description: Orama Network - Distributed P2P Database System
|
Description: Orama Network - Distributed P2P Database System
|
||||||
Orama is a distributed peer-to-peer network that combines
|
Orama is a distributed peer-to-peer network that combines
|
||||||
RQLite for distributed SQL, IPFS for content-addressed storage,
|
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()
|
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
|
-- DNS records table for dynamic DNS management
|
||||||
CREATE TABLE IF NOT EXISTS dns_records (
|
CREATE TABLE IF NOT EXISTS dns_records (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
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
|
record_type TEXT NOT NULL DEFAULT 'A', -- DNS record type: A, AAAA, CNAME, TXT
|
||||||
value TEXT NOT NULL, -- IP address or target value
|
value TEXT NOT NULL, -- IP address or target value
|
||||||
ttl INTEGER NOT NULL DEFAULT 300, -- Time to live in seconds
|
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
|
-- Seed reserved domains
|
||||||
INSERT INTO reserved_domains (domain, reason) VALUES
|
INSERT INTO reserved_domains (domain, reason) VALUES
|
||||||
('api.debros.network', 'API gateway endpoint'),
|
('api.orama.network', 'API gateway endpoint'),
|
||||||
('www.debros.network', 'Marketing website'),
|
('www.orama.network', 'Marketing website'),
|
||||||
('admin.debros.network', 'Admin panel'),
|
('admin.orama.network', 'Admin panel'),
|
||||||
('ns1.debros.network', 'Nameserver 1'),
|
('ns1.orama.network', 'Nameserver 1'),
|
||||||
('ns2.debros.network', 'Nameserver 2'),
|
('ns2.orama.network', 'Nameserver 2'),
|
||||||
('ns3.debros.network', 'Nameserver 3'),
|
('ns3.orama.network', 'Nameserver 3'),
|
||||||
('ns4.debros.network', 'Nameserver 4'),
|
('ns4.orama.network', 'Nameserver 4'),
|
||||||
('mail.debros.network', 'Email service'),
|
('mail.orama.network', 'Email service'),
|
||||||
('cdn.debros.network', 'Content delivery'),
|
('cdn.orama.network', 'Content delivery'),
|
||||||
('docs.debros.network', 'Documentation'),
|
('docs.orama.network', 'Documentation'),
|
||||||
('status.debros.network', 'Status page')
|
('status.orama.network', 'Status page')
|
||||||
ON CONFLICT(domain) DO NOTHING;
|
ON CONFLICT(domain) DO NOTHING;
|
||||||
|
|
||||||
-- Mark migration as applied
|
-- Mark migration as applied
|
||||||
|
|||||||
@ -83,7 +83,7 @@ CREATE TABLE IF NOT EXISTS deployment_domains (
|
|||||||
id TEXT PRIMARY KEY, -- UUID
|
id TEXT PRIMARY KEY, -- UUID
|
||||||
deployment_id TEXT NOT NULL,
|
deployment_id TEXT NOT NULL,
|
||||||
namespace 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'
|
routing_type TEXT NOT NULL DEFAULT 'balanced', -- 'balanced' or 'node_specific'
|
||||||
node_id TEXT, -- For node_specific routing
|
node_id TEXT, -- For node_specific routing
|
||||||
is_custom BOOLEAN DEFAULT FALSE, -- True for user's own domain
|
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"
|
endpoint := gatewayURL + "/v1/auth/simple-key"
|
||||||
|
|
||||||
// Extract domain from URL for TLS configuration
|
// 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)
|
domain := extractDomainFromURL(gatewayURL)
|
||||||
client := tlsutil.NewHTTPClientForDomain(30*time.Second, domain)
|
client := tlsutil.NewHTTPClientForDomain(30*time.Second, domain)
|
||||||
|
|
||||||
|
|||||||
@ -179,11 +179,11 @@ func (cm *CertificateManager) generateNodeCertificate(hostname string, caCertPEM
|
|||||||
DNSNames: []string{hostname},
|
DNSNames: []string{hostname},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add wildcard support if hostname contains *.debros.network
|
// Add wildcard support if hostname contains *.orama.network
|
||||||
if hostname == "*.debros.network" {
|
if hostname == "*.orama.network" {
|
||||||
template.DNSNames = []string{"*.debros.network", "debros.network"}
|
template.DNSNames = []string{"*.orama.network", "orama.network"}
|
||||||
} else if hostname == "debros.network" {
|
} else if hostname == "orama.network" {
|
||||||
template.DNSNames = []string{"*.debros.network", "debros.network"}
|
template.DNSNames = []string{"*.orama.network", "orama.network"}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to parse as IP address for IP-based certificates
|
// 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) {
|
func LoadTLSCertificate(certPEM, keyPEM []byte) (tls.Certificate, error) {
|
||||||
return tls.X509KeyPair(certPEM, keyPEM)
|
return tls.X509KeyPair(certPEM, keyPEM)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -192,7 +192,7 @@ func promptForGatewayURL() string {
|
|||||||
return "http://localhost:6001"
|
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, _ := reader.ReadString('\n')
|
||||||
domain = strings.TrimSpace(domain)
|
domain = strings.TrimSpace(domain)
|
||||||
|
|
||||||
|
|||||||
@ -439,7 +439,7 @@ func getAPIURL() string {
|
|||||||
if url := os.Getenv("ORAMA_API_URL"); url != "" {
|
if url := os.Getenv("ORAMA_API_URL"); url != "" {
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
return "https://gateway.debros.network"
|
return "https://gateway.orama.network"
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAuthToken() (string, error) {
|
func getAuthToken() (string, error) {
|
||||||
|
|||||||
@ -387,7 +387,7 @@ func getAPIURL() string {
|
|||||||
if url := os.Getenv("ORAMA_API_URL"); url != "" {
|
if url := os.Getenv("ORAMA_API_URL"); url != "" {
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
return "https://gateway.debros.network"
|
return "https://gateway.orama.network"
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAuthToken() (string, error) {
|
func getAuthToken() (string, error) {
|
||||||
|
|||||||
@ -8,7 +8,7 @@ The plugin provides:
|
|||||||
- **Dynamic DNS Records**: Queries RQLite for DNS records in real-time
|
- **Dynamic DNS Records**: Queries RQLite for DNS records in real-time
|
||||||
- **Caching**: In-memory cache to reduce database load
|
- **Caching**: In-memory cache to reduce database load
|
||||||
- **Health Monitoring**: Periodic health checks of RQLite connection
|
- **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
|
## Building CoreDNS with RQLite Plugin
|
||||||
|
|
||||||
@ -146,7 +146,7 @@ sudo systemctl start coredns
|
|||||||
The Corefile at `/etc/coredns/Corefile` configures CoreDNS behavior:
|
The Corefile at `/etc/coredns/Corefile` configures CoreDNS behavior:
|
||||||
|
|
||||||
```corefile
|
```corefile
|
||||||
debros.network {
|
orama.network {
|
||||||
rqlite {
|
rqlite {
|
||||||
dsn http://localhost:5001 # RQLite HTTP endpoint
|
dsn http://localhost:5001 # RQLite HTTP endpoint
|
||||||
refresh 10s # Health check interval
|
refresh 10s # Health check interval
|
||||||
@ -196,7 +196,7 @@ curl -XPOST 'http://localhost:5001/db/execute' \
|
|||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-d '[
|
-d '[
|
||||||
["INSERT INTO dns_records (fqdn, record_type, value, ttl, namespace, created_by, is_active) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
["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
|
```bash
|
||||||
# Query local CoreDNS
|
# Query local CoreDNS
|
||||||
dig @localhost test.debros.network
|
dig @localhost test.orama.network
|
||||||
|
|
||||||
# Expected output:
|
# Expected output:
|
||||||
# ;; ANSWER SECTION:
|
# ;; 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
|
# Query from remote machine
|
||||||
dig @<node-ip> test.debros.network
|
dig @<node-ip> test.orama.network
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Test Wildcard
|
### 3. Test Wildcard
|
||||||
@ -222,12 +222,12 @@ curl -XPOST 'http://localhost:5001/db/execute' \
|
|||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-d '[
|
-d '[
|
||||||
["INSERT INTO dns_records (fqdn, record_type, value, ttl, namespace, created_by, is_active) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
["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
|
# Test wildcard resolution
|
||||||
dig @localhost app1.node-abc123.debros.network
|
dig @localhost app1.node-abc123.orama.network
|
||||||
dig @localhost app2.node-abc123.debros.network
|
dig @localhost app2.node-abc123.orama.network
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Check Health
|
### 4. Check Health
|
||||||
@ -295,7 +295,7 @@ sudo ufw status | grep 5001
|
|||||||
sudo netstat -tulpn | grep :53
|
sudo netstat -tulpn | grep :53
|
||||||
|
|
||||||
# 2. Test local query
|
# 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
|
# 3. Check logs for errors
|
||||||
sudo journalctl -u coredns --since "5 minutes ago"
|
sudo journalctl -u coredns --since "5 minutes ago"
|
||||||
@ -327,38 +327,38 @@ Install CoreDNS on all 4 nameserver nodes (ns1-ns4).
|
|||||||
|
|
||||||
### 2. Configure Registrar
|
### 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.
|
orama.network. IN NS ns1.orama.network.
|
||||||
debros.network. IN NS ns2.debros.network.
|
orama.network. IN NS ns2.orama.network.
|
||||||
debros.network. IN NS ns3.debros.network.
|
orama.network. IN NS ns3.orama.network.
|
||||||
debros.network. IN NS ns4.debros.network.
|
orama.network. IN NS ns4.orama.network.
|
||||||
```
|
```
|
||||||
|
|
||||||
Add glue records:
|
Add glue records:
|
||||||
|
|
||||||
```
|
```
|
||||||
ns1.debros.network. IN A <node-1-ip>
|
ns1.orama.network. IN A <node-1-ip>
|
||||||
ns2.debros.network. IN A <node-2-ip>
|
ns2.orama.network. IN A <node-2-ip>
|
||||||
ns3.debros.network. IN A <node-3-ip>
|
ns3.orama.network. IN A <node-3-ip>
|
||||||
ns4.debros.network. IN A <node-4-ip>
|
ns4.orama.network. IN A <node-4-ip>
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Verify Propagation
|
### 3. Verify Propagation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Check NS records
|
# Check NS records
|
||||||
dig NS debros.network
|
dig NS orama.network
|
||||||
|
|
||||||
# Check from public DNS
|
# Check from public DNS
|
||||||
dig @8.8.8.8 test.debros.network
|
dig @8.8.8.8 test.orama.network
|
||||||
|
|
||||||
# Check from all nameservers
|
# Check from all nameservers
|
||||||
dig @ns1.debros.network test.debros.network
|
dig @ns1.orama.network test.orama.network
|
||||||
dig @ns2.debros.network test.debros.network
|
dig @ns2.orama.network test.orama.network
|
||||||
dig @ns3.debros.network test.debros.network
|
dig @ns3.orama.network test.orama.network
|
||||||
dig @ns4.debros.network test.debros.network
|
dig @ns4.orama.network test.orama.network
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Monitor
|
### 4. Monitor
|
||||||
|
|||||||
@ -2,7 +2,6 @@ package rqlite
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coredns/coredns/plugin"
|
"github.com/coredns/coredns/plugin"
|
||||||
@ -13,11 +12,11 @@ import (
|
|||||||
|
|
||||||
// RQLitePlugin implements the CoreDNS plugin interface
|
// RQLitePlugin implements the CoreDNS plugin interface
|
||||||
type RQLitePlugin struct {
|
type RQLitePlugin struct {
|
||||||
Next plugin.Handler
|
Next plugin.Handler
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
backend *Backend
|
backend *Backend
|
||||||
cache *Cache
|
cache *Cache
|
||||||
zones []string
|
zones []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name returns the plugin name
|
// 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
|
// 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 {
|
func (p *RQLitePlugin) getWildcardName(qname string) string {
|
||||||
labels := dns.SplitDomainName(qname)
|
labels := dns.SplitDomainName(qname)
|
||||||
if len(labels) < 3 {
|
if len(labels) < 3 {
|
||||||
|
|||||||
@ -62,8 +62,8 @@ func (h *DomainHandler) HandleAddDomain(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if domain is reserved
|
// Check if domain is reserved
|
||||||
if strings.HasSuffix(domain, ".debros.network") {
|
if strings.HasSuffix(domain, ".orama.network") {
|
||||||
http.Error(w, "Cannot use .debros.network domains as custom domains", http.StatusBadRequest)
|
http.Error(w, "Cannot use .orama.network domains as custom domains", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,8 +165,8 @@ func (h *DomainHandler) HandleVerifyDomain(w http.ResponseWriter, r *http.Reques
|
|||||||
|
|
||||||
// Get domain record
|
// Get domain record
|
||||||
type domainRow struct {
|
type domainRow struct {
|
||||||
DeploymentID string `db:"deployment_id"`
|
DeploymentID string `db:"deployment_id"`
|
||||||
VerificationToken string `db:"verification_token"`
|
VerificationToken string `db:"verification_token"`
|
||||||
VerificationStatus string `db:"verification_status"`
|
VerificationStatus string `db:"verification_status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -258,9 +258,9 @@ func (h *DomainHandler) HandleListDomains(w http.ResponseWriter, r *http.Request
|
|||||||
|
|
||||||
// Query domains
|
// Query domains
|
||||||
type domainRow struct {
|
type domainRow struct {
|
||||||
Domain string `db:"domain"`
|
Domain string `db:"domain"`
|
||||||
VerificationStatus string `db:"verification_status"`
|
VerificationStatus string `db:"verification_status"`
|
||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
VerifiedAt *time.Time `db:"verified_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)
|
namespace := ctx.Value("namespace").(string)
|
||||||
|
|
||||||
type deploymentRow struct {
|
type deploymentRow struct {
|
||||||
ID string `db:"id"`
|
ID string `db:"id"`
|
||||||
Namespace string `db:"namespace"`
|
Namespace string `db:"namespace"`
|
||||||
Name string `db:"name"`
|
Name string `db:"name"`
|
||||||
Type string `db:"type"`
|
Type string `db:"type"`
|
||||||
Version int `db:"version"`
|
Version int `db:"version"`
|
||||||
Status string `db:"status"`
|
Status string `db:"status"`
|
||||||
ContentCID string `db:"content_cid"`
|
ContentCID string `db:"content_cid"`
|
||||||
HomeNodeID string `db:"home_node_id"`
|
HomeNodeID string `db:"home_node_id"`
|
||||||
Port int `db:"port"`
|
Port int `db:"port"`
|
||||||
Subdomain string `db:"subdomain"`
|
Subdomain string `db:"subdomain"`
|
||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
UpdatedAt time.Time `db:"updated_at"`
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var rows []deploymentRow
|
var rows []deploymentRow
|
||||||
@ -61,10 +61,10 @@ func (h *ListHandler) HandleList(w http.ResponseWriter, r *http.Request) {
|
|||||||
deployments := make([]map[string]interface{}, len(rows))
|
deployments := make([]map[string]interface{}, len(rows))
|
||||||
for i, row := range rows {
|
for i, row := range rows {
|
||||||
urls := []string{
|
urls := []string{
|
||||||
"https://" + row.Name + "." + row.HomeNodeID + ".debros.network",
|
"https://" + row.Name + "." + row.HomeNodeID + ".orama.network",
|
||||||
}
|
}
|
||||||
if row.Subdomain != "" {
|
if row.Subdomain != "" {
|
||||||
urls = append(urls, "https://"+row.Subdomain+".debros.network")
|
urls = append(urls, "https://"+row.Subdomain+".orama.network")
|
||||||
}
|
}
|
||||||
|
|
||||||
deployments[i] = map[string]interface{}{
|
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
|
// 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 {
|
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))
|
s.logger.Error("Failed to create node-specific DNS record", zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create load-balanced record if subdomain is set
|
// Create load-balanced record if subdomain is set
|
||||||
if deployment.Subdomain != "" {
|
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 {
|
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))
|
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
|
// BuildDeploymentURLs builds all URLs for a deployment
|
||||||
func (s *DeploymentService) BuildDeploymentURLs(deployment *deployments.Deployment) []string {
|
func (s *DeploymentService) BuildDeploymentURLs(deployment *deployments.Deployment) []string {
|
||||||
urls := []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 != "" {
|
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
|
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) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
host := strings.Split(r.Host, ":")[0] // Strip port
|
host := strings.Split(r.Host, ":")[0] // Strip port
|
||||||
|
|
||||||
// Only process .debros.network domains
|
// Only process .orama.network domains
|
||||||
if !strings.HasSuffix(host, ".debros.network") {
|
if !strings.HasSuffix(host, ".orama.network") {
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
return
|
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
|
SELECT d.id, d.namespace, d.name, d.type, d.port, d.content_cid, d.status
|
||||||
FROM deployments d
|
FROM deployments d
|
||||||
LEFT JOIN deployment_domains dd ON d.id = dd.deployment_id
|
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')
|
OR dd.domain = ? AND dd.verification_status = 'verified')
|
||||||
AND d.status = 'active'
|
AND d.status = 'active'
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
|
|||||||
@ -26,3 +26,110 @@ func TestExtractAPIKey(t *testing.T) {
|
|||||||
t.Fatalf("got %q", got)
|
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
|
// Global cache of trusted domains loaded from environment
|
||||||
trustedDomains []string
|
trustedDomains []string
|
||||||
// CA certificate pool for trusting self-signed certs
|
// CA certificate pool for trusting self-signed certs
|
||||||
caCertPool *x509.CertPool
|
caCertPool *x509.CertPool
|
||||||
initialized bool
|
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{
|
var defaultTrustedDomains = []string{
|
||||||
"*.debros.network",
|
"*.orama.network",
|
||||||
}
|
}
|
||||||
|
|
||||||
// init loads trusted domains and CA certificate from environment and files
|
// init loads trusted domains and CA certificate from environment and files
|
||||||
@ -64,7 +64,7 @@ func GetTrustedDomains() []string {
|
|||||||
func ShouldSkipTLSVerify(domain string) bool {
|
func ShouldSkipTLSVerify(domain string) bool {
|
||||||
for _, trusted := range trustedDomains {
|
for _, trusted := range trustedDomains {
|
||||||
if strings.HasPrefix(trusted, "*.") {
|
if strings.HasPrefix(trusted, "*.") {
|
||||||
// Handle wildcards like *.debros.network
|
// Handle wildcards like *.orama.network
|
||||||
suffix := strings.TrimPrefix(trusted, "*")
|
suffix := strings.TrimPrefix(trusted, "*")
|
||||||
if strings.HasSuffix(domain, suffix) || domain == strings.TrimPrefix(suffix, ".") {
|
if strings.HasSuffix(domain, suffix) || domain == strings.TrimPrefix(suffix, ".") {
|
||||||
return true
|
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="${NODES[$i]}"
|
||||||
node_num=$((i + 1))
|
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
|
# Copy binary
|
||||||
echo " → Copying binary..."
|
echo " → Copying binary..."
|
||||||
@ -59,9 +59,9 @@ for i in "${!NODES[@]}"; do
|
|||||||
# Check status
|
# Check status
|
||||||
echo " → Checking status..."
|
echo " → Checking status..."
|
||||||
if ssh "debros@$node" "sudo systemctl is-active --quiet coredns"; then
|
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
|
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"
|
echo " Check logs: ssh debros@$node sudo journalctl -u coredns -n 50"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@ -71,14 +71,14 @@ done
|
|||||||
echo "✅ Deployment complete!"
|
echo "✅ Deployment complete!"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Next steps:"
|
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 " 2. Update registrar NS records (ONLY after testing):"
|
||||||
echo " NS debros.network. ns1.debros.network."
|
echo " NS orama.network. ns1.orama.network."
|
||||||
echo " NS debros.network. ns2.debros.network."
|
echo " NS orama.network. ns2.orama.network."
|
||||||
echo " NS debros.network. ns3.debros.network."
|
echo " NS orama.network. ns3.orama.network."
|
||||||
echo " NS debros.network. ns4.debros.network."
|
echo " NS orama.network. ns4.orama.network."
|
||||||
echo " A ns1.debros.network. ${NODES[0]}"
|
echo " A ns1.orama.network. ${NODES[0]}"
|
||||||
echo " A ns2.debros.network. ${NODES[1]}"
|
echo " A ns2.orama.network. ${NODES[1]}"
|
||||||
echo " A ns3.debros.network. ${NODES[2]}"
|
echo " A ns3.orama.network. ${NODES[2]}"
|
||||||
echo " A ns4.debros.network. ${NODES[3]}"
|
echo " A ns4.orama.network. ${NODES[3]}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
@ -124,7 +124,7 @@ log_info " sudo systemctl status coredns"
|
|||||||
log_info " sudo journalctl -u coredns -f"
|
log_info " sudo journalctl -u coredns -f"
|
||||||
echo
|
echo
|
||||||
log_info "To test DNS:"
|
log_info "To test DNS:"
|
||||||
log_info " dig @localhost test.debros.network"
|
log_info " dig @localhost test.orama.network"
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
rm -f /tmp/coredns.tgz
|
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