From 0dcde29f7ceca2009fce09d5923d62902a658870 Mon Sep 17 00:00:00 2001 From: anonpenguin23 Date: Thu, 22 Jan 2026 14:39:50 +0200 Subject: [PATCH] added some tests --- .github/workflows/release-apt.yml | 2 +- docs/TESTING_PLAN.md | 760 ++++++++ e2e/deployments/static_deployment_test.go | 194 ++ e2e/domain_routing_test.go | 257 +++ e2e/env.go | 368 ++++ e2e/fullstack_integration_test.go | 292 +++ e2e/namespace_isolation_test.go | 423 ++++ migrations/005_dns_records.sql | 24 +- migrations/007_deployments.sql | 2 +- pkg/auth/simple_auth.go | 2 +- pkg/certutil/cert_manager.go | 11 +- pkg/cli/auth_commands.go | 2 +- pkg/cli/db/commands.go | 2 +- pkg/cli/deployments/deploy.go | 2 +- pkg/coredns/README.md | 50 +- pkg/coredns/rqlite/plugin.go | 11 +- .../handlers/deployments/domain_handler.go | 14 +- .../handlers/deployments/handlers_test.go | 398 ++++ .../handlers/deployments/list_handler.go | 28 +- .../handlers/deployments/mocks_test.go | 239 +++ pkg/gateway/handlers/deployments/service.go | 8 +- pkg/gateway/handlers/sqlite/handlers_test.go | 522 +++++ pkg/gateway/middleware.go | 6 +- pkg/gateway/middleware_test.go | 107 + pkg/tlsutil/client.go | 11 +- scripts/deploy-coredns.sh | 24 +- scripts/install-coredns.sh | 2 +- testdata/.gitignore | 15 + testdata/README.md | 138 ++ testdata/apps/go-backend/Makefile | 10 + testdata/apps/go-backend/go.mod | 3 + testdata/apps/go-backend/main.go | 109 ++ .../apps/nextjs-ssr/app/api/data/route.ts | 26 + .../apps/nextjs-ssr/app/api/hello/route.ts | 9 + testdata/apps/nextjs-ssr/app/globals.css | 72 + testdata/apps/nextjs-ssr/app/layout.tsx | 19 + testdata/apps/nextjs-ssr/app/page.tsx | 23 + testdata/apps/nextjs-ssr/next-env.d.ts | 5 + testdata/apps/nextjs-ssr/next.config.js | 6 + testdata/apps/nextjs-ssr/package-lock.json | 501 +++++ testdata/apps/nextjs-ssr/package.json | 21 + testdata/apps/nextjs-ssr/tsconfig.json | 27 + testdata/apps/react-vite/index.html | 12 + testdata/apps/react-vite/package-lock.json | 1716 +++++++++++++++++ testdata/apps/react-vite/package.json | 21 + testdata/apps/react-vite/src/App.css | 48 + testdata/apps/react-vite/src/App.jsx | 28 + testdata/apps/react-vite/src/index.css | 40 + testdata/apps/react-vite/src/main.jsx | 10 + testdata/apps/react-vite/vite.config.js | 10 + testdata/build-fixtures.sh | 58 + 51 files changed, 6586 insertions(+), 102 deletions(-) create mode 100644 docs/TESTING_PLAN.md create mode 100644 e2e/deployments/static_deployment_test.go create mode 100644 e2e/domain_routing_test.go create mode 100644 e2e/fullstack_integration_test.go create mode 100644 e2e/namespace_isolation_test.go create mode 100644 pkg/gateway/handlers/deployments/handlers_test.go create mode 100644 pkg/gateway/handlers/deployments/mocks_test.go create mode 100644 pkg/gateway/handlers/sqlite/handlers_test.go create mode 100644 testdata/.gitignore create mode 100644 testdata/README.md create mode 100644 testdata/apps/go-backend/Makefile create mode 100644 testdata/apps/go-backend/go.mod create mode 100644 testdata/apps/go-backend/main.go create mode 100644 testdata/apps/nextjs-ssr/app/api/data/route.ts create mode 100644 testdata/apps/nextjs-ssr/app/api/hello/route.ts create mode 100644 testdata/apps/nextjs-ssr/app/globals.css create mode 100644 testdata/apps/nextjs-ssr/app/layout.tsx create mode 100644 testdata/apps/nextjs-ssr/app/page.tsx create mode 100644 testdata/apps/nextjs-ssr/next-env.d.ts create mode 100644 testdata/apps/nextjs-ssr/next.config.js create mode 100644 testdata/apps/nextjs-ssr/package-lock.json create mode 100644 testdata/apps/nextjs-ssr/package.json create mode 100644 testdata/apps/nextjs-ssr/tsconfig.json create mode 100644 testdata/apps/react-vite/index.html create mode 100644 testdata/apps/react-vite/package-lock.json create mode 100644 testdata/apps/react-vite/package.json create mode 100644 testdata/apps/react-vite/src/App.css create mode 100644 testdata/apps/react-vite/src/App.jsx create mode 100644 testdata/apps/react-vite/src/index.css create mode 100644 testdata/apps/react-vite/src/main.jsx create mode 100644 testdata/apps/react-vite/vite.config.js create mode 100755 testdata/build-fixtures.sh diff --git a/.github/workflows/release-apt.yml b/.github/workflows/release-apt.yml index d5e361e..f8b9a91 100644 --- a/.github/workflows/release-apt.yml +++ b/.github/workflows/release-apt.yml @@ -82,7 +82,7 @@ jobs: Priority: optional Architecture: ${ARCH} Depends: libc6 - Maintainer: DeBros Team + Maintainer: DeBros Team Description: Orama Network - Distributed P2P Database System Orama is a distributed peer-to-peer network that combines RQLite for distributed SQL, IPFS for content-addressed storage, diff --git a/docs/TESTING_PLAN.md b/docs/TESTING_PLAN.md new file mode 100644 index 0000000..1abdc3c --- /dev/null +++ b/docs/TESTING_PLAN.md @@ -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 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 +``` diff --git a/e2e/deployments/static_deployment_test.go b/e2e/deployments/static_deployment_test.go new file mode 100644 index 0000000..440903e --- /dev/null +++ b/e2e/deployments/static_deployment_test.go @@ -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, "
", "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), "
", "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 +} diff --git a/e2e/domain_routing_test.go b/e2e/domain_routing_test.go new file mode 100644 index 0000000..7fc9dde --- /dev/null +++ b/e2e/domain_routing_test.go @@ -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), "
", "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, "
", + "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), "
", + "SPA fallback should return index.html for %s", path) + } + + t.Logf("✓ SPA fallback routing verified for %d paths", len(unknownPaths)) + }) +} diff --git a/e2e/env.go b/e2e/env.go index 0beff18..56f45e8 100644 --- a/e2e/env.go +++ b/e2e/env.go @@ -966,3 +966,371 @@ func (p *WSPubSubClientPair) Close() { p.Subscriber.Close() } } + +// ============================================================================ +// Deployment Testing Helpers +// ============================================================================ + +// E2ETestEnv holds the environment configuration for deployment E2E tests +type E2ETestEnv struct { + GatewayURL string + APIKey string + Namespace string + HTTPClient *http.Client + SkipCleanup bool +} + +// LoadTestEnv loads the test environment from environment variables +func LoadTestEnv() (*E2ETestEnv, error) { + gatewayURL := os.Getenv("ORAMA_GATEWAY_URL") + if gatewayURL == "" { + gatewayURL = GetGatewayURL() + } + + apiKey := os.Getenv("ORAMA_API_KEY") + if apiKey == "" { + apiKey = GetAPIKey() + } + + namespace := os.Getenv("ORAMA_NAMESPACE") + if namespace == "" { + namespace = GetClientNamespace() + } + + skipCleanup := os.Getenv("ORAMA_SKIP_CLEANUP") == "true" + + return &E2ETestEnv{ + GatewayURL: gatewayURL, + APIKey: apiKey, + Namespace: namespace, + HTTPClient: NewHTTPClient(30 * time.Second), + SkipCleanup: skipCleanup, + }, nil +} + +// LoadTestEnvWithNamespace loads test environment with a specific namespace +func LoadTestEnvWithNamespace(namespace string) (*E2ETestEnv, error) { + env, err := LoadTestEnv() + if err != nil { + return nil, err + } + env.Namespace = namespace + return env, nil +} + +// CreateTestDeployment creates a test deployment and returns its ID +func CreateTestDeployment(t *testing.T, env *E2ETestEnv, name, tarballPath string) string { + t.Helper() + + file, err := os.Open(tarballPath) + if err != nil { + t.Fatalf("failed to open tarball: %v", err) + } + defer file.Close() + + // Create multipart form + body := &bytes.Buffer{} + boundary := "----WebKitFormBoundary7MA4YWxkTrZu0gW" + + // Write name field + body.WriteString("--" + boundary + "\r\n") + body.WriteString("Content-Disposition: form-data; name=\"name\"\r\n\r\n") + body.WriteString(name + "\r\n") + + // Write subdomain field + body.WriteString("--" + boundary + "\r\n") + body.WriteString("Content-Disposition: form-data; name=\"subdomain\"\r\n\r\n") + body.WriteString(name + "\r\n") + + // Write tarball file + body.WriteString("--" + boundary + "\r\n") + body.WriteString("Content-Disposition: form-data; name=\"tarball\"; filename=\"app.tar.gz\"\r\n") + body.WriteString("Content-Type: application/gzip\r\n\r\n") + + fileData, _ := io.ReadAll(file) + body.Write(fileData) + body.WriteString("\r\n--" + boundary + "--\r\n") + + req, err := http.NewRequest("POST", env.GatewayURL+"/v1/deployments/static/upload", body) + if err != nil { + t.Fatalf("failed to create request: %v", err) + } + + req.Header.Set("Content-Type", "multipart/form-data; boundary="+boundary) + req.Header.Set("Authorization", "Bearer "+env.APIKey) + + resp, err := env.HTTPClient.Do(req) + if err != nil { + t.Fatalf("failed to upload deployment: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusCreated { + bodyBytes, _ := io.ReadAll(resp.Body) + t.Fatalf("deployment upload failed with status %d: %s", resp.StatusCode, string(bodyBytes)) + } + + var result map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + t.Fatalf("failed to decode response: %v", err) + } + + return result["id"].(string) +} + +// DeleteDeployment deletes a deployment by ID +func DeleteDeployment(t *testing.T, env *E2ETestEnv, deploymentID string) { + t.Helper() + + req, _ := http.NewRequest("DELETE", env.GatewayURL+"/v1/deployments/delete?id="+deploymentID, nil) + req.Header.Set("Authorization", "Bearer "+env.APIKey) + + resp, err := env.HTTPClient.Do(req) + if err != nil { + t.Logf("warning: failed to delete deployment: %v", err) + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Logf("warning: delete deployment returned status %d", resp.StatusCode) + } +} + +// GetDeployment retrieves deployment metadata by ID +func GetDeployment(t *testing.T, env *E2ETestEnv, deploymentID string) map[string]interface{} { + t.Helper() + + req, _ := http.NewRequest("GET", env.GatewayURL+"/v1/deployments/get?id="+deploymentID, nil) + req.Header.Set("Authorization", "Bearer "+env.APIKey) + + resp, err := env.HTTPClient.Do(req) + if err != nil { + t.Fatalf("failed to get deployment: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + bodyBytes, _ := io.ReadAll(resp.Body) + t.Fatalf("get deployment failed with status %d: %s", resp.StatusCode, string(bodyBytes)) + } + + var deployment map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&deployment); err != nil { + t.Fatalf("failed to decode deployment: %v", err) + } + + return deployment +} + +// CreateSQLiteDB creates a SQLite database for a namespace +func CreateSQLiteDB(t *testing.T, env *E2ETestEnv, dbName string) { + t.Helper() + + reqBody := map[string]string{"database_name": dbName} + bodyBytes, _ := json.Marshal(reqBody) + + req, _ := http.NewRequest("POST", env.GatewayURL+"/v1/db/sqlite/create", bytes.NewReader(bodyBytes)) + req.Header.Set("Authorization", "Bearer "+env.APIKey) + req.Header.Set("Content-Type", "application/json") + + resp, err := env.HTTPClient.Do(req) + if err != nil { + t.Fatalf("failed to create database: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusCreated { + bodyBytes, _ := io.ReadAll(resp.Body) + t.Fatalf("create database failed with status %d: %s", resp.StatusCode, string(bodyBytes)) + } +} + +// DeleteSQLiteDB deletes a SQLite database +func DeleteSQLiteDB(t *testing.T, env *E2ETestEnv, dbName string) { + t.Helper() + + reqBody := map[string]string{"database_name": dbName} + bodyBytes, _ := json.Marshal(reqBody) + + req, _ := http.NewRequest("DELETE", env.GatewayURL+"/v1/db/sqlite/delete", bytes.NewReader(bodyBytes)) + req.Header.Set("Authorization", "Bearer "+env.APIKey) + req.Header.Set("Content-Type", "application/json") + + resp, err := env.HTTPClient.Do(req) + if err != nil { + t.Logf("warning: failed to delete database: %v", err) + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Logf("warning: delete database returned status %d", resp.StatusCode) + } +} + +// ExecuteSQLQuery executes a SQL query on a database +func ExecuteSQLQuery(t *testing.T, env *E2ETestEnv, dbName, query string) map[string]interface{} { + t.Helper() + + reqBody := map[string]interface{}{ + "database_name": dbName, + "query": query, + } + bodyBytes, _ := json.Marshal(reqBody) + + req, _ := http.NewRequest("POST", env.GatewayURL+"/v1/db/sqlite/query", bytes.NewReader(bodyBytes)) + req.Header.Set("Authorization", "Bearer "+env.APIKey) + req.Header.Set("Content-Type", "application/json") + + resp, err := env.HTTPClient.Do(req) + if err != nil { + t.Fatalf("failed to execute query: %v", err) + } + defer resp.Body.Close() + + var result map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + t.Fatalf("failed to decode query response: %v", err) + } + + if errMsg, ok := result["error"].(string); ok && errMsg != "" { + t.Fatalf("SQL query failed: %s", errMsg) + } + + return result +} + +// QuerySQLite executes a SELECT query and returns rows +func QuerySQLite(t *testing.T, env *E2ETestEnv, dbName, query string) []map[string]interface{} { + t.Helper() + + result := ExecuteSQLQuery(t, env, dbName, query) + + rows, ok := result["rows"].([]interface{}) + if !ok { + return []map[string]interface{}{} + } + + columns, _ := result["columns"].([]interface{}) + + var results []map[string]interface{} + for _, row := range rows { + rowData, ok := row.([]interface{}) + if !ok { + continue + } + + rowMap := make(map[string]interface{}) + for i, col := range columns { + if i < len(rowData) { + rowMap[col.(string)] = rowData[i] + } + } + results = append(results, rowMap) + } + + return results +} + +// UploadTestFile uploads a file to IPFS and returns the CID +func UploadTestFile(t *testing.T, env *E2ETestEnv, filename, content string) string { + t.Helper() + + body := &bytes.Buffer{} + boundary := "----WebKitFormBoundary7MA4YWxkTrZu0gW" + + body.WriteString("--" + boundary + "\r\n") + body.WriteString(fmt.Sprintf("Content-Disposition: form-data; name=\"file\"; filename=\"%s\"\r\n", filename)) + body.WriteString("Content-Type: text/plain\r\n\r\n") + body.WriteString(content) + body.WriteString("\r\n--" + boundary + "--\r\n") + + req, _ := http.NewRequest("POST", env.GatewayURL+"/v1/storage/upload", body) + req.Header.Set("Content-Type", "multipart/form-data; boundary="+boundary) + req.Header.Set("Authorization", "Bearer "+env.APIKey) + + resp, err := env.HTTPClient.Do(req) + if err != nil { + t.Fatalf("failed to upload file: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + bodyBytes, _ := io.ReadAll(resp.Body) + t.Fatalf("upload file failed with status %d: %s", resp.StatusCode, string(bodyBytes)) + } + + var result map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + t.Fatalf("failed to decode upload response: %v", err) + } + + cid, ok := result["cid"].(string) + if !ok { + t.Fatalf("CID not found in response") + } + + return cid +} + +// UnpinFile unpins a file from IPFS +func UnpinFile(t *testing.T, env *E2ETestEnv, cid string) { + t.Helper() + + reqBody := map[string]string{"cid": cid} + bodyBytes, _ := json.Marshal(reqBody) + + req, _ := http.NewRequest("POST", env.GatewayURL+"/v1/storage/unpin", bytes.NewReader(bodyBytes)) + req.Header.Set("Authorization", "Bearer "+env.APIKey) + req.Header.Set("Content-Type", "application/json") + + resp, err := env.HTTPClient.Do(req) + if err != nil { + t.Logf("warning: failed to unpin file: %v", err) + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Logf("warning: unpin file returned status %d", resp.StatusCode) + } +} + +// TestDeploymentWithHostHeader tests a deployment by setting the Host header +func TestDeploymentWithHostHeader(t *testing.T, env *E2ETestEnv, host, path string) *http.Response { + t.Helper() + + req, err := http.NewRequest("GET", env.GatewayURL+path, nil) + if err != nil { + t.Fatalf("failed to create request: %v", err) + } + + req.Host = host + + resp, err := env.HTTPClient.Do(req) + if err != nil { + t.Fatalf("failed to test deployment: %v", err) + } + + return resp +} + +// WaitForHealthy waits for a deployment to become healthy +func WaitForHealthy(t *testing.T, env *E2ETestEnv, deploymentID string, timeout time.Duration) bool { + t.Helper() + + deadline := time.Now().Add(timeout) + + for time.Now().Before(deadline) { + deployment := GetDeployment(t, env, deploymentID) + + if status, ok := deployment["status"].(string); ok && status == "active" { + return true + } + + time.Sleep(1 * time.Second) + } + + return false +} diff --git a/e2e/fullstack_integration_test.go b/e2e/fullstack_integration_test.go new file mode 100644 index 0000000..ae9f755 --- /dev/null +++ b/e2e/fullstack_integration_test.go @@ -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), "
", "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) + }) +} diff --git a/e2e/namespace_isolation_test.go b/e2e/namespace_isolation_test.go new file mode 100644 index 0000000..4e1ecbb --- /dev/null +++ b/e2e/namespace_isolation_test.go @@ -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) + }) +} diff --git a/migrations/005_dns_records.sql b/migrations/005_dns_records.sql index f05d3a9..650e07a 100644 --- a/migrations/005_dns_records.sql +++ b/migrations/005_dns_records.sql @@ -6,7 +6,7 @@ BEGIN; -- DNS records table for dynamic DNS management CREATE TABLE IF NOT EXISTS dns_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, - fqdn TEXT NOT NULL UNIQUE, -- Fully qualified domain name (e.g., myapp.node-7prvNa.debros.network) + fqdn TEXT NOT NULL UNIQUE, -- Fully qualified domain name (e.g., myapp.node-7prvNa.orama.network) record_type TEXT NOT NULL DEFAULT 'A', -- DNS record type: A, AAAA, CNAME, TXT value TEXT NOT NULL, -- IP address or target value ttl INTEGER NOT NULL DEFAULT 300, -- Time to live in seconds @@ -53,17 +53,17 @@ CREATE TABLE IF NOT EXISTS reserved_domains ( -- Seed reserved domains INSERT INTO reserved_domains (domain, reason) VALUES - ('api.debros.network', 'API gateway endpoint'), - ('www.debros.network', 'Marketing website'), - ('admin.debros.network', 'Admin panel'), - ('ns1.debros.network', 'Nameserver 1'), - ('ns2.debros.network', 'Nameserver 2'), - ('ns3.debros.network', 'Nameserver 3'), - ('ns4.debros.network', 'Nameserver 4'), - ('mail.debros.network', 'Email service'), - ('cdn.debros.network', 'Content delivery'), - ('docs.debros.network', 'Documentation'), - ('status.debros.network', 'Status page') + ('api.orama.network', 'API gateway endpoint'), + ('www.orama.network', 'Marketing website'), + ('admin.orama.network', 'Admin panel'), + ('ns1.orama.network', 'Nameserver 1'), + ('ns2.orama.network', 'Nameserver 2'), + ('ns3.orama.network', 'Nameserver 3'), + ('ns4.orama.network', 'Nameserver 4'), + ('mail.orama.network', 'Email service'), + ('cdn.orama.network', 'Content delivery'), + ('docs.orama.network', 'Documentation'), + ('status.orama.network', 'Status page') ON CONFLICT(domain) DO NOTHING; -- Mark migration as applied diff --git a/migrations/007_deployments.sql b/migrations/007_deployments.sql index 139462f..9690640 100644 --- a/migrations/007_deployments.sql +++ b/migrations/007_deployments.sql @@ -83,7 +83,7 @@ CREATE TABLE IF NOT EXISTS deployment_domains ( id TEXT PRIMARY KEY, -- UUID deployment_id TEXT NOT NULL, namespace TEXT NOT NULL, - domain TEXT NOT NULL UNIQUE, -- Full domain (e.g., myapp.debros.network or custom) + domain TEXT NOT NULL UNIQUE, -- Full domain (e.g., myapp.orama.network or custom) routing_type TEXT NOT NULL DEFAULT 'balanced', -- 'balanced' or 'node_specific' node_id TEXT, -- For node_specific routing is_custom BOOLEAN DEFAULT FALSE, -- True for user's own domain diff --git a/pkg/auth/simple_auth.go b/pkg/auth/simple_auth.go index af11953..597e84f 100644 --- a/pkg/auth/simple_auth.go +++ b/pkg/auth/simple_auth.go @@ -95,7 +95,7 @@ func requestAPIKeyFromGateway(gatewayURL, wallet, namespace string) (string, err endpoint := gatewayURL + "/v1/auth/simple-key" // Extract domain from URL for TLS configuration - // This uses tlsutil which handles Let's Encrypt staging certificates for *.debros.network + // This uses tlsutil which handles Let's Encrypt staging certificates for *.orama.network domain := extractDomainFromURL(gatewayURL) client := tlsutil.NewHTTPClientForDomain(30*time.Second, domain) diff --git a/pkg/certutil/cert_manager.go b/pkg/certutil/cert_manager.go index db484e5..7a23949 100644 --- a/pkg/certutil/cert_manager.go +++ b/pkg/certutil/cert_manager.go @@ -179,11 +179,11 @@ func (cm *CertificateManager) generateNodeCertificate(hostname string, caCertPEM DNSNames: []string{hostname}, } - // Add wildcard support if hostname contains *.debros.network - if hostname == "*.debros.network" { - template.DNSNames = []string{"*.debros.network", "debros.network"} - } else if hostname == "debros.network" { - template.DNSNames = []string{"*.debros.network", "debros.network"} + // Add wildcard support if hostname contains *.orama.network + if hostname == "*.orama.network" { + template.DNSNames = []string{"*.orama.network", "orama.network"} + } else if hostname == "orama.network" { + template.DNSNames = []string{"*.orama.network", "orama.network"} } // Try to parse as IP address for IP-based certificates @@ -254,4 +254,3 @@ func (cm *CertificateManager) parseCACertificate(caCertPEM, caKeyPEM []byte) (*x func LoadTLSCertificate(certPEM, keyPEM []byte) (tls.Certificate, error) { return tls.X509KeyPair(certPEM, keyPEM) } - diff --git a/pkg/cli/auth_commands.go b/pkg/cli/auth_commands.go index 36f8594..79b027b 100644 --- a/pkg/cli/auth_commands.go +++ b/pkg/cli/auth_commands.go @@ -192,7 +192,7 @@ func promptForGatewayURL() string { return "http://localhost:6001" } - fmt.Print("Enter node domain (e.g., node-hk19de.debros.network): ") + fmt.Print("Enter node domain (e.g., node-hk19de.orama.network): ") domain, _ := reader.ReadString('\n') domain = strings.TrimSpace(domain) diff --git a/pkg/cli/db/commands.go b/pkg/cli/db/commands.go index 672cc0d..9cdb9b6 100644 --- a/pkg/cli/db/commands.go +++ b/pkg/cli/db/commands.go @@ -439,7 +439,7 @@ func getAPIURL() string { if url := os.Getenv("ORAMA_API_URL"); url != "" { return url } - return "https://gateway.debros.network" + return "https://gateway.orama.network" } func getAuthToken() (string, error) { diff --git a/pkg/cli/deployments/deploy.go b/pkg/cli/deployments/deploy.go index 73bcc23..12980eb 100644 --- a/pkg/cli/deployments/deploy.go +++ b/pkg/cli/deployments/deploy.go @@ -387,7 +387,7 @@ func getAPIURL() string { if url := os.Getenv("ORAMA_API_URL"); url != "" { return url } - return "https://gateway.debros.network" + return "https://gateway.orama.network" } func getAuthToken() (string, error) { diff --git a/pkg/coredns/README.md b/pkg/coredns/README.md index ee18147..9d5d562 100644 --- a/pkg/coredns/README.md +++ b/pkg/coredns/README.md @@ -8,7 +8,7 @@ The plugin provides: - **Dynamic DNS Records**: Queries RQLite for DNS records in real-time - **Caching**: In-memory cache to reduce database load - **Health Monitoring**: Periodic health checks of RQLite connection -- **Wildcard Support**: Handles wildcard DNS patterns (e.g., `*.node-xyz.debros.network`) +- **Wildcard Support**: Handles wildcard DNS patterns (e.g., `*.node-xyz.orama.network`) ## Building CoreDNS with RQLite Plugin @@ -146,7 +146,7 @@ sudo systemctl start coredns The Corefile at `/etc/coredns/Corefile` configures CoreDNS behavior: ```corefile -debros.network { +orama.network { rqlite { dsn http://localhost:5001 # RQLite HTTP endpoint refresh 10s # Health check interval @@ -196,7 +196,7 @@ curl -XPOST 'http://localhost:5001/db/execute' \ -H 'Content-Type: application/json' \ -d '[ ["INSERT INTO dns_records (fqdn, record_type, value, ttl, namespace, created_by, is_active) VALUES (?, ?, ?, ?, ?, ?, ?)", - "test.debros.network.", "A", "1.2.3.4", 300, "test", "system", true] + "test.orama.network.", "A", "1.2.3.4", 300, "test", "system", true] ]' ``` @@ -204,14 +204,14 @@ curl -XPOST 'http://localhost:5001/db/execute' \ ```bash # Query local CoreDNS -dig @localhost test.debros.network +dig @localhost test.orama.network # Expected output: # ;; ANSWER SECTION: -# test.debros.network. 300 IN A 1.2.3.4 +# test.orama.network. 300 IN A 1.2.3.4 # Query from remote machine -dig @ test.debros.network +dig @ test.orama.network ``` ### 3. Test Wildcard @@ -222,12 +222,12 @@ curl -XPOST 'http://localhost:5001/db/execute' \ -H 'Content-Type: application/json' \ -d '[ ["INSERT INTO dns_records (fqdn, record_type, value, ttl, namespace, created_by, is_active) VALUES (?, ?, ?, ?, ?, ?, ?)", - "*.node-abc123.debros.network.", "A", "1.2.3.4", 300, "test", "system", true] + "*.node-abc123.orama.network.", "A", "1.2.3.4", 300, "test", "system", true] ]' # Test wildcard resolution -dig @localhost app1.node-abc123.debros.network -dig @localhost app2.node-abc123.debros.network +dig @localhost app1.node-abc123.orama.network +dig @localhost app2.node-abc123.orama.network ``` ### 4. Check Health @@ -295,7 +295,7 @@ sudo ufw status | grep 5001 sudo netstat -tulpn | grep :53 # 2. Test local query -dig @127.0.0.1 test.debros.network +dig @127.0.0.1 test.orama.network # 3. Check logs for errors sudo journalctl -u coredns --since "5 minutes ago" @@ -327,38 +327,38 @@ Install CoreDNS on all 4 nameserver nodes (ns1-ns4). ### 2. Configure Registrar -At your domain registrar, set NS records for `debros.network`: +At your domain registrar, set NS records for `orama.network`: ``` -debros.network. IN NS ns1.debros.network. -debros.network. IN NS ns2.debros.network. -debros.network. IN NS ns3.debros.network. -debros.network. IN NS ns4.debros.network. +orama.network. IN NS ns1.orama.network. +orama.network. IN NS ns2.orama.network. +orama.network. IN NS ns3.orama.network. +orama.network. IN NS ns4.orama.network. ``` Add glue records: ``` -ns1.debros.network. IN A -ns2.debros.network. IN A -ns3.debros.network. IN A -ns4.debros.network. IN A +ns1.orama.network. IN A +ns2.orama.network. IN A +ns3.orama.network. IN A +ns4.orama.network. IN A ``` ### 3. Verify Propagation ```bash # Check NS records -dig NS debros.network +dig NS orama.network # Check from public DNS -dig @8.8.8.8 test.debros.network +dig @8.8.8.8 test.orama.network # Check from all nameservers -dig @ns1.debros.network test.debros.network -dig @ns2.debros.network test.debros.network -dig @ns3.debros.network test.debros.network -dig @ns4.debros.network test.debros.network +dig @ns1.orama.network test.orama.network +dig @ns2.orama.network test.orama.network +dig @ns3.orama.network test.orama.network +dig @ns4.orama.network test.orama.network ``` ### 4. Monitor diff --git a/pkg/coredns/rqlite/plugin.go b/pkg/coredns/rqlite/plugin.go index f8d8d80..434a2f5 100644 --- a/pkg/coredns/rqlite/plugin.go +++ b/pkg/coredns/rqlite/plugin.go @@ -2,7 +2,6 @@ package rqlite import ( "context" - "fmt" "time" "github.com/coredns/coredns/plugin" @@ -13,11 +12,11 @@ import ( // RQLitePlugin implements the CoreDNS plugin interface type RQLitePlugin struct { - Next plugin.Handler - logger *zap.Logger + Next plugin.Handler + logger *zap.Logger backend *Backend - cache *Cache - zones []string + cache *Cache + zones []string } // Name returns the plugin name @@ -110,7 +109,7 @@ func (p *RQLitePlugin) isOurZone(qname string) bool { } // getWildcardName extracts the wildcard pattern for a given name -// e.g., myapp.node-7prvNa.debros.network -> *.node-7prvNa.debros.network +// e.g., myapp.node-7prvNa.orama.network -> *.node-7prvNa.orama.network func (p *RQLitePlugin) getWildcardName(qname string) string { labels := dns.SplitDomainName(qname) if len(labels) < 3 { diff --git a/pkg/gateway/handlers/deployments/domain_handler.go b/pkg/gateway/handlers/deployments/domain_handler.go index 8476f2d..a313c41 100644 --- a/pkg/gateway/handlers/deployments/domain_handler.go +++ b/pkg/gateway/handlers/deployments/domain_handler.go @@ -62,8 +62,8 @@ func (h *DomainHandler) HandleAddDomain(w http.ResponseWriter, r *http.Request) } // Check if domain is reserved - if strings.HasSuffix(domain, ".debros.network") { - http.Error(w, "Cannot use .debros.network domains as custom domains", http.StatusBadRequest) + if strings.HasSuffix(domain, ".orama.network") { + http.Error(w, "Cannot use .orama.network domains as custom domains", http.StatusBadRequest) return } @@ -165,8 +165,8 @@ func (h *DomainHandler) HandleVerifyDomain(w http.ResponseWriter, r *http.Reques // Get domain record type domainRow struct { - DeploymentID string `db:"deployment_id"` - VerificationToken string `db:"verification_token"` + DeploymentID string `db:"deployment_id"` + VerificationToken string `db:"verification_token"` VerificationStatus string `db:"verification_status"` } @@ -258,9 +258,9 @@ func (h *DomainHandler) HandleListDomains(w http.ResponseWriter, r *http.Request // Query domains type domainRow struct { - Domain string `db:"domain"` - VerificationStatus string `db:"verification_status"` - CreatedAt time.Time `db:"created_at"` + Domain string `db:"domain"` + VerificationStatus string `db:"verification_status"` + CreatedAt time.Time `db:"created_at"` VerifiedAt *time.Time `db:"verified_at"` } diff --git a/pkg/gateway/handlers/deployments/handlers_test.go b/pkg/gateway/handlers/deployments/handlers_test.go new file mode 100644 index 0000000..4735efe --- /dev/null +++ b/pkg/gateway/handlers/deployments/handlers_test.go @@ -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 := "Test" + + // 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 := "SPA" + 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) + } +} diff --git a/pkg/gateway/handlers/deployments/list_handler.go b/pkg/gateway/handlers/deployments/list_handler.go index 4479efc..86d71f1 100644 --- a/pkg/gateway/handlers/deployments/list_handler.go +++ b/pkg/gateway/handlers/deployments/list_handler.go @@ -29,18 +29,18 @@ func (h *ListHandler) HandleList(w http.ResponseWriter, r *http.Request) { namespace := ctx.Value("namespace").(string) type deploymentRow struct { - ID string `db:"id"` - Namespace string `db:"namespace"` - Name string `db:"name"` - Type string `db:"type"` - Version int `db:"version"` - Status string `db:"status"` - ContentCID string `db:"content_cid"` - HomeNodeID string `db:"home_node_id"` - Port int `db:"port"` - Subdomain string `db:"subdomain"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` + ID string `db:"id"` + Namespace string `db:"namespace"` + Name string `db:"name"` + Type string `db:"type"` + Version int `db:"version"` + Status string `db:"status"` + ContentCID string `db:"content_cid"` + HomeNodeID string `db:"home_node_id"` + Port int `db:"port"` + Subdomain string `db:"subdomain"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` } var rows []deploymentRow @@ -61,10 +61,10 @@ func (h *ListHandler) HandleList(w http.ResponseWriter, r *http.Request) { deployments := make([]map[string]interface{}, len(rows)) for i, row := range rows { urls := []string{ - "https://" + row.Name + "." + row.HomeNodeID + ".debros.network", + "https://" + row.Name + "." + row.HomeNodeID + ".orama.network", } if row.Subdomain != "" { - urls = append(urls, "https://"+row.Subdomain+".debros.network") + urls = append(urls, "https://"+row.Subdomain+".orama.network") } deployments[i] = map[string]interface{}{ diff --git a/pkg/gateway/handlers/deployments/mocks_test.go b/pkg/gateway/handlers/deployments/mocks_test.go new file mode 100644 index 0000000..4797a80 --- /dev/null +++ b/pkg/gateway/handlers/deployments/mocks_test.go @@ -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 +} diff --git a/pkg/gateway/handlers/deployments/service.go b/pkg/gateway/handlers/deployments/service.go index 3fc9e93..631e57b 100644 --- a/pkg/gateway/handlers/deployments/service.go +++ b/pkg/gateway/handlers/deployments/service.go @@ -179,14 +179,14 @@ func (s *DeploymentService) CreateDNSRecords(ctx context.Context, deployment *de } // Create node-specific record - nodeFQDN := fmt.Sprintf("%s.%s.debros.network.", deployment.Name, deployment.HomeNodeID) + nodeFQDN := fmt.Sprintf("%s.%s.orama.network.", deployment.Name, deployment.HomeNodeID) if err := s.createDNSRecord(ctx, nodeFQDN, "A", nodeIP, deployment.Namespace, deployment.ID); err != nil { s.logger.Error("Failed to create node-specific DNS record", zap.Error(err)) } // Create load-balanced record if subdomain is set if deployment.Subdomain != "" { - lbFQDN := fmt.Sprintf("%s.debros.network.", deployment.Subdomain) + lbFQDN := fmt.Sprintf("%s.orama.network.", deployment.Subdomain) if err := s.createDNSRecord(ctx, lbFQDN, "A", nodeIP, deployment.Namespace, deployment.ID); err != nil { s.logger.Error("Failed to create load-balanced DNS record", zap.Error(err)) } @@ -231,11 +231,11 @@ func (s *DeploymentService) getNodeIP(ctx context.Context, nodeID string) (strin // BuildDeploymentURLs builds all URLs for a deployment func (s *DeploymentService) BuildDeploymentURLs(deployment *deployments.Deployment) []string { urls := []string{ - fmt.Sprintf("https://%s.%s.debros.network", deployment.Name, deployment.HomeNodeID), + fmt.Sprintf("https://%s.%s.orama.network", deployment.Name, deployment.HomeNodeID), } if deployment.Subdomain != "" { - urls = append(urls, fmt.Sprintf("https://%s.debros.network", deployment.Subdomain)) + urls = append(urls, fmt.Sprintf("https://%s.orama.network", deployment.Subdomain)) } return urls diff --git a/pkg/gateway/handlers/sqlite/handlers_test.go b/pkg/gateway/handlers/sqlite/handlers_test.go new file mode 100644 index 0000000..14e2673 --- /dev/null +++ b/pkg/gateway/handlers/sqlite/handlers_test.go @@ -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) + } + } +} diff --git a/pkg/gateway/middleware.go b/pkg/gateway/middleware.go index ab73e4c..4255af1 100644 --- a/pkg/gateway/middleware.go +++ b/pkg/gateway/middleware.go @@ -439,8 +439,8 @@ func (g *Gateway) domainRoutingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { host := strings.Split(r.Host, ":")[0] // Strip port - // Only process .debros.network domains - if !strings.HasSuffix(host, ".debros.network") { + // Only process .orama.network domains + if !strings.HasSuffix(host, ".orama.network") { next.ServeHTTP(w, r) return } @@ -497,7 +497,7 @@ func (g *Gateway) getDeploymentByDomain(ctx context.Context, domain string) (*de SELECT d.id, d.namespace, d.name, d.type, d.port, d.content_cid, d.status FROM deployments d LEFT JOIN deployment_domains dd ON d.id = dd.deployment_id - WHERE (d.name || '.node-' || d.home_node_id || '.debros.network' = ? + WHERE (d.name || '.node-' || d.home_node_id || '.orama.network' = ? OR dd.domain = ? AND dd.verification_status = 'verified') AND d.status = 'active' LIMIT 1 diff --git a/pkg/gateway/middleware_test.go b/pkg/gateway/middleware_test.go index 91e2b5a..7202445 100644 --- a/pkg/gateway/middleware_test.go +++ b/pkg/gateway/middleware_test.go @@ -26,3 +26,110 @@ func TestExtractAPIKey(t *testing.T) { t.Fatalf("got %q", got) } } + +// TestDomainRoutingMiddleware_NonDebrosNetwork tests that non-debros domains pass through +func TestDomainRoutingMiddleware_NonDebrosNetwork(t *testing.T) { + nextCalled := false + next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + nextCalled = true + w.WriteHeader(http.StatusOK) + }) + + g := &Gateway{} + middleware := g.domainRoutingMiddleware(next) + + req := httptest.NewRequest("GET", "/", nil) + req.Host = "example.com" + + rr := httptest.NewRecorder() + middleware.ServeHTTP(rr, req) + + if !nextCalled { + t.Error("Expected next handler to be called for non-debros domain") + } + + if rr.Code != http.StatusOK { + t.Errorf("Expected status 200, got %d", rr.Code) + } +} + +// TestDomainRoutingMiddleware_APIPathBypass tests that /v1/ paths bypass routing +func TestDomainRoutingMiddleware_APIPathBypass(t *testing.T) { + nextCalled := false + next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + nextCalled = true + w.WriteHeader(http.StatusOK) + }) + + g := &Gateway{} + middleware := g.domainRoutingMiddleware(next) + + req := httptest.NewRequest("GET", "/v1/deployments/list", nil) + req.Host = "myapp.orama.network" + + rr := httptest.NewRecorder() + middleware.ServeHTTP(rr, req) + + if !nextCalled { + t.Error("Expected next handler to be called for /v1/ path") + } + + if rr.Code != http.StatusOK { + t.Errorf("Expected status 200, got %d", rr.Code) + } +} + +// TestDomainRoutingMiddleware_WellKnownBypass tests that /.well-known/ paths bypass routing +func TestDomainRoutingMiddleware_WellKnownBypass(t *testing.T) { + nextCalled := false + next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + nextCalled = true + w.WriteHeader(http.StatusOK) + }) + + g := &Gateway{} + middleware := g.domainRoutingMiddleware(next) + + req := httptest.NewRequest("GET", "/.well-known/acme-challenge/test", nil) + req.Host = "myapp.orama.network" + + rr := httptest.NewRecorder() + middleware.ServeHTTP(rr, req) + + if !nextCalled { + t.Error("Expected next handler to be called for /.well-known/ path") + } + + if rr.Code != http.StatusOK { + t.Errorf("Expected status 200, got %d", rr.Code) + } +} + +// TestDomainRoutingMiddleware_NoDeploymentService tests graceful handling when deployment service is nil +func TestDomainRoutingMiddleware_NoDeploymentService(t *testing.T) { + nextCalled := false + next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + nextCalled = true + w.WriteHeader(http.StatusOK) + }) + + g := &Gateway{ + // deploymentService is nil + staticHandler: nil, + } + middleware := g.domainRoutingMiddleware(next) + + req := httptest.NewRequest("GET", "/", nil) + req.Host = "myapp.orama.network" + + rr := httptest.NewRecorder() + middleware.ServeHTTP(rr, req) + + if !nextCalled { + t.Error("Expected next handler to be called when deployment service is nil") + } + + if rr.Code != http.StatusOK { + t.Errorf("Expected status 200, got %d", rr.Code) + } +} diff --git a/pkg/tlsutil/client.go b/pkg/tlsutil/client.go index 735ce8e..28feadf 100644 --- a/pkg/tlsutil/client.go +++ b/pkg/tlsutil/client.go @@ -14,13 +14,13 @@ var ( // Global cache of trusted domains loaded from environment trustedDomains []string // CA certificate pool for trusting self-signed certs - caCertPool *x509.CertPool - initialized bool + caCertPool *x509.CertPool + initialized bool ) -// Default trusted domains - always trust debros.network for staging/development +// Default trusted domains - always trust orama.network for staging/development var defaultTrustedDomains = []string{ - "*.debros.network", + "*.orama.network", } // init loads trusted domains and CA certificate from environment and files @@ -64,7 +64,7 @@ func GetTrustedDomains() []string { func ShouldSkipTLSVerify(domain string) bool { for _, trusted := range trustedDomains { if strings.HasPrefix(trusted, "*.") { - // Handle wildcards like *.debros.network + // Handle wildcards like *.orama.network suffix := strings.TrimPrefix(trusted, "*") if strings.HasSuffix(domain, suffix) || domain == strings.TrimPrefix(suffix, ".") { return true @@ -119,4 +119,3 @@ func NewHTTPClientForDomain(timeout time.Duration, hostname string) *http.Client }, } } - diff --git a/scripts/deploy-coredns.sh b/scripts/deploy-coredns.sh index ffd65d5..9b291b3 100755 --- a/scripts/deploy-coredns.sh +++ b/scripts/deploy-coredns.sh @@ -32,7 +32,7 @@ for i in "${!NODES[@]}"; do node="${NODES[$i]}" node_num=$((i + 1)) - echo "[$node_num/4] Deploying to ns${node_num}.debros.network ($node)..." + echo "[$node_num/4] Deploying to ns${node_num}.orama.network ($node)..." # Copy binary echo " → Copying binary..." @@ -59,9 +59,9 @@ for i in "${!NODES[@]}"; do # Check status echo " → Checking status..." if ssh "debros@$node" "sudo systemctl is-active --quiet coredns"; then - echo " ✅ CoreDNS running on ns${node_num}.debros.network" + echo " ✅ CoreDNS running on ns${node_num}.orama.network" else - echo " ❌ CoreDNS failed to start on ns${node_num}.debros.network" + echo " ❌ CoreDNS failed to start on ns${node_num}.orama.network" echo " Check logs: ssh debros@$node sudo journalctl -u coredns -n 50" fi @@ -71,14 +71,14 @@ done echo "✅ Deployment complete!" echo "" echo "Next steps:" -echo " 1. Test DNS resolution: dig @${NODES[0]} test.debros.network" +echo " 1. Test DNS resolution: dig @${NODES[0]} test.orama.network" echo " 2. Update registrar NS records (ONLY after testing):" -echo " NS debros.network. ns1.debros.network." -echo " NS debros.network. ns2.debros.network." -echo " NS debros.network. ns3.debros.network." -echo " NS debros.network. ns4.debros.network." -echo " A ns1.debros.network. ${NODES[0]}" -echo " A ns2.debros.network. ${NODES[1]}" -echo " A ns3.debros.network. ${NODES[2]}" -echo " A ns4.debros.network. ${NODES[3]}" +echo " NS orama.network. ns1.orama.network." +echo " NS orama.network. ns2.orama.network." +echo " NS orama.network. ns3.orama.network." +echo " NS orama.network. ns4.orama.network." +echo " A ns1.orama.network. ${NODES[0]}" +echo " A ns2.orama.network. ${NODES[1]}" +echo " A ns3.orama.network. ${NODES[2]}" +echo " A ns4.orama.network. ${NODES[3]}" echo "" diff --git a/scripts/install-coredns.sh b/scripts/install-coredns.sh index 11b7bd2..0fdbd09 100755 --- a/scripts/install-coredns.sh +++ b/scripts/install-coredns.sh @@ -124,7 +124,7 @@ log_info " sudo systemctl status coredns" log_info " sudo journalctl -u coredns -f" echo log_info "To test DNS:" -log_info " dig @localhost test.debros.network" +log_info " dig @localhost test.orama.network" # Cleanup rm -f /tmp/coredns.tgz diff --git a/testdata/.gitignore b/testdata/.gitignore new file mode 100644 index 0000000..16697ef --- /dev/null +++ b/testdata/.gitignore @@ -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 diff --git a/testdata/README.md b/testdata/README.md new file mode 100644 index 0000000..0be59e4 --- /dev/null +++ b/testdata/README.md @@ -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 diff --git a/testdata/apps/go-backend/Makefile b/testdata/apps/go-backend/Makefile new file mode 100644 index 0000000..0c70f49 --- /dev/null +++ b/testdata/apps/go-backend/Makefile @@ -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 diff --git a/testdata/apps/go-backend/go.mod b/testdata/apps/go-backend/go.mod new file mode 100644 index 0000000..b7c06a5 --- /dev/null +++ b/testdata/apps/go-backend/go.mod @@ -0,0 +1,3 @@ +module github.com/DeBrosOfficial/network/testdata/apps/go-backend + +go 1.21 diff --git a/testdata/apps/go-backend/main.go b/testdata/apps/go-backend/main.go new file mode 100644 index 0000000..744eca1 --- /dev/null +++ b/testdata/apps/go-backend/main.go @@ -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)) +} diff --git a/testdata/apps/nextjs-ssr/app/api/data/route.ts b/testdata/apps/nextjs-ssr/app/api/data/route.ts new file mode 100644 index 0000000..9bec71e --- /dev/null +++ b/testdata/apps/nextjs-ssr/app/api/data/route.ts @@ -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 }) +} diff --git a/testdata/apps/nextjs-ssr/app/api/hello/route.ts b/testdata/apps/nextjs-ssr/app/api/hello/route.ts new file mode 100644 index 0000000..f3f07a8 --- /dev/null +++ b/testdata/apps/nextjs-ssr/app/api/hello/route.ts @@ -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' + }) +} diff --git a/testdata/apps/nextjs-ssr/app/globals.css b/testdata/apps/nextjs-ssr/app/globals.css new file mode 100644 index 0000000..6d10315 --- /dev/null +++ b/testdata/apps/nextjs-ssr/app/globals.css @@ -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; +} diff --git a/testdata/apps/nextjs-ssr/app/layout.tsx b/testdata/apps/nextjs-ssr/app/layout.tsx new file mode 100644 index 0000000..708d9e8 --- /dev/null +++ b/testdata/apps/nextjs-ssr/app/layout.tsx @@ -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 ( + + {children} + + ) +} diff --git a/testdata/apps/nextjs-ssr/app/page.tsx b/testdata/apps/nextjs-ssr/app/page.tsx new file mode 100644 index 0000000..7642944 --- /dev/null +++ b/testdata/apps/nextjs-ssr/app/page.tsx @@ -0,0 +1,23 @@ +export default function Home() { + return ( +
+

Orama Network Next.js Test

+

+ E2E Testing - Next.js SSR Deployment +

+
+

Server-Side Rendering Test

+

This page is rendered on the server.

+

Current time: {new Date().toISOString()}

+
+
+

API Routes:

+ +
+

Version: 1.0.0

+
+ ) +} diff --git a/testdata/apps/nextjs-ssr/next-env.d.ts b/testdata/apps/nextjs-ssr/next-env.d.ts new file mode 100644 index 0000000..40c3d68 --- /dev/null +++ b/testdata/apps/nextjs-ssr/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/testdata/apps/nextjs-ssr/next.config.js b/testdata/apps/nextjs-ssr/next.config.js new file mode 100644 index 0000000..5cd8cc3 --- /dev/null +++ b/testdata/apps/nextjs-ssr/next.config.js @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: 'standalone', +} + +module.exports = nextConfig diff --git a/testdata/apps/nextjs-ssr/package-lock.json b/testdata/apps/nextjs-ssr/package-lock.json new file mode 100644 index 0000000..582b4fe --- /dev/null +++ b/testdata/apps/nextjs-ssr/package-lock.json @@ -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" + } + } +} diff --git a/testdata/apps/nextjs-ssr/package.json b/testdata/apps/nextjs-ssr/package.json new file mode 100644 index 0000000..e82a2d5 --- /dev/null +++ b/testdata/apps/nextjs-ssr/package.json @@ -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" + } +} diff --git a/testdata/apps/nextjs-ssr/tsconfig.json b/testdata/apps/nextjs-ssr/tsconfig.json new file mode 100644 index 0000000..d8b9323 --- /dev/null +++ b/testdata/apps/nextjs-ssr/tsconfig.json @@ -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"] +} diff --git a/testdata/apps/react-vite/index.html b/testdata/apps/react-vite/index.html new file mode 100644 index 0000000..b97bbdf --- /dev/null +++ b/testdata/apps/react-vite/index.html @@ -0,0 +1,12 @@ + + + + + + Orama Network Test App + + +
+ + + diff --git a/testdata/apps/react-vite/package-lock.json b/testdata/apps/react-vite/package-lock.json new file mode 100644 index 0000000..67673e3 --- /dev/null +++ b/testdata/apps/react-vite/package-lock.json @@ -0,0 +1,1716 @@ +{ + "name": "react-vite-test", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "react-vite-test", + "version": "1.0.0", + "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" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.56.0.tgz", + "integrity": "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.56.0.tgz", + "integrity": "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.56.0.tgz", + "integrity": "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.56.0.tgz", + "integrity": "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.56.0.tgz", + "integrity": "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.56.0.tgz", + "integrity": "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.56.0.tgz", + "integrity": "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.56.0.tgz", + "integrity": "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.56.0.tgz", + "integrity": "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.56.0.tgz", + "integrity": "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.56.0.tgz", + "integrity": "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.56.0.tgz", + "integrity": "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.56.0.tgz", + "integrity": "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.56.0.tgz", + "integrity": "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.56.0.tgz", + "integrity": "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.56.0.tgz", + "integrity": "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.56.0.tgz", + "integrity": "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.56.0.tgz", + "integrity": "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.56.0.tgz", + "integrity": "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.56.0.tgz", + "integrity": "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.56.0.tgz", + "integrity": "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.56.0.tgz", + "integrity": "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.56.0.tgz", + "integrity": "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.56.0.tgz", + "integrity": "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.56.0.tgz", + "integrity": "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "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/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.17", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.17.tgz", + "integrity": "sha512-agD0MgJFUP/4nvjqzIB29zRPUuCF7Ge6mEv9s8dHrtYD7QWXRcx75rOADE/d5ah1NI+0vkDl0yorDd5U852IQQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "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==", + "dev": true, + "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/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "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/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.277", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.277.tgz", + "integrity": "sha512-wKXFZw4erWmmOz5N/grBoJ2XrNJGDFMu2+W5ACHza5rHtvsqrK4gb6rnLC7XxKB9WlJ+RmyQatuEXmtm86xbnw==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "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/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "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/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "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/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "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", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.56.0.tgz", + "integrity": "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.56.0", + "@rollup/rollup-android-arm64": "4.56.0", + "@rollup/rollup-darwin-arm64": "4.56.0", + "@rollup/rollup-darwin-x64": "4.56.0", + "@rollup/rollup-freebsd-arm64": "4.56.0", + "@rollup/rollup-freebsd-x64": "4.56.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.56.0", + "@rollup/rollup-linux-arm-musleabihf": "4.56.0", + "@rollup/rollup-linux-arm64-gnu": "4.56.0", + "@rollup/rollup-linux-arm64-musl": "4.56.0", + "@rollup/rollup-linux-loong64-gnu": "4.56.0", + "@rollup/rollup-linux-loong64-musl": "4.56.0", + "@rollup/rollup-linux-ppc64-gnu": "4.56.0", + "@rollup/rollup-linux-ppc64-musl": "4.56.0", + "@rollup/rollup-linux-riscv64-gnu": "4.56.0", + "@rollup/rollup-linux-riscv64-musl": "4.56.0", + "@rollup/rollup-linux-s390x-gnu": "4.56.0", + "@rollup/rollup-linux-x64-gnu": "4.56.0", + "@rollup/rollup-linux-x64-musl": "4.56.0", + "@rollup/rollup-openbsd-x64": "4.56.0", + "@rollup/rollup-openharmony-arm64": "4.56.0", + "@rollup/rollup-win32-arm64-msvc": "4.56.0", + "@rollup/rollup-win32-ia32-msvc": "4.56.0", + "@rollup/rollup-win32-x64-gnu": "4.56.0", + "@rollup/rollup-win32-x64-msvc": "4.56.0", + "fsevents": "~2.3.2" + } + }, + "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/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/testdata/apps/react-vite/package.json b/testdata/apps/react-vite/package.json new file mode 100644 index 0000000..f1bdf29 --- /dev/null +++ b/testdata/apps/react-vite/package.json @@ -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" + } +} diff --git a/testdata/apps/react-vite/src/App.css b/testdata/apps/react-vite/src/App.css new file mode 100644 index 0000000..2e1f43d --- /dev/null +++ b/testdata/apps/react-vite/src/App.css @@ -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; +} diff --git a/testdata/apps/react-vite/src/App.jsx b/testdata/apps/react-vite/src/App.jsx new file mode 100644 index 0000000..a5f0909 --- /dev/null +++ b/testdata/apps/react-vite/src/App.jsx @@ -0,0 +1,28 @@ +import { useState } from 'react' +import './App.css' + +function App() { + const [count, setCount] = useState(0) + + return ( + <> +
+

Orama Network Test App

+

+ E2E Testing - React Vite Static Deployment +

+
+ +

+ This is a test application for validating static site deployments. +

+
+

Version: 1.0.0

+
+ + ) +} + +export default App diff --git a/testdata/apps/react-vite/src/index.css b/testdata/apps/react-vite/src/index.css new file mode 100644 index 0000000..5876d1c --- /dev/null +++ b/testdata/apps/react-vite/src/index.css @@ -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; + } +} diff --git a/testdata/apps/react-vite/src/main.jsx b/testdata/apps/react-vite/src/main.jsx new file mode 100644 index 0000000..54b39dd --- /dev/null +++ b/testdata/apps/react-vite/src/main.jsx @@ -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( + + + , +) diff --git a/testdata/apps/react-vite/vite.config.js b/testdata/apps/react-vite/vite.config.js new file mode 100644 index 0000000..d6e6a0a --- /dev/null +++ b/testdata/apps/react-vite/vite.config.js @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true + } +}) diff --git a/testdata/build-fixtures.sh b/testdata/build-fixtures.sh new file mode 100755 index 0000000..aecdea2 --- /dev/null +++ b/testdata/build-fixtures.sh @@ -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!"