Compare commits

...

5 Commits

13 changed files with 2349 additions and 17 deletions

View File

@@ -1,5 +1,5 @@
# Build stage # Build stage
FROM golang:1.23-alpine AS builder FROM golang:1.24-alpine AS builder
RUN apk add --no-cache gcc musl-dev sqlite-dev RUN apk add --no-cache gcc musl-dev sqlite-dev

58
Makefile Normal file
View File

@@ -0,0 +1,58 @@
.PHONY: build test test-unit test-integration test-compose run clean docker-build docker-push help
# Variables
BINARY_NAME=dyn-server
DOCKER_IMAGE=git.dws.rip/DWS/dyn
VERSION=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
help: ## Show this help message
@echo "Available targets:"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'
build: ## Build the binary
go build -o $(BINARY_NAME) cmd/server/main.go
test: ## Run all tests
go test ./... -v
test-unit: ## Run unit tests only
go test ./internal/... -v
test-integration: ## Run integration tests (with mock server)
go test ./tests/integration -v -run 'TestIntegration_[^C]'
test-compose: ## Run containerized integration tests with podman/docker-compose
@echo "Running compose integration tests..."
bash tests/integration/compose_test.sh
test-compose-go: ## Run Go-based compose integration tests
RUN_COMPOSE_TESTS=true go test ./tests/integration -v -run TestComposeIntegration -timeout 120s
run: build ## Build and run the server
./$(BINARY_NAME)
dev: ## Run in development mode
go run cmd/server/main.go
docker-build: ## Build Docker image
docker build -t $(DOCKER_IMAGE):$(VERSION) -t $(DOCKER_IMAGE):latest .
docker-push: docker-build ## Push Docker image
docker push $(DOCKER_IMAGE):$(VERSION)
docker push $(DOCKER_IMAGE):latest
clean: ## Clean build artifacts
rm -f $(BINARY_NAME)
go clean
deps: ## Download dependencies
go mod download
go mod tidy
lint: ## Run linter (requires golangci-lint)
golangci-lint run ./...
fmt: ## Format Go code
go fmt ./...
.DEFAULT_GOAL := help

View File

@@ -230,8 +230,85 @@ go run cmd/server/main.go
# Build # Build
go build -o server cmd/server/main.go go build -o server cmd/server/main.go
# Test # Run all tests
go test ./... go test ./...
# Run tests with verbose output
go test ./... -v
# Run specific test package
go test ./internal/handlers -v
go test ./internal/database -v
go test ./tests/integration -v
# Run with coverage
go test ./... -cover
```
## Testing
The project includes comprehensive tests:
### Unit Tests
- **config**: Configuration loading and validation (11 tests)
- **database**: Database operations and models (9 tests)
- **handlers**: Handler logic, custom filters, and validation (36 tests)
### Integration Tests
Full end-to-end tests using:
- **Mock Technitium Server**: Simulates DNS API without external dependencies
- **SQLite**: In-memory/temp database for isolated testing
- **Gin Test Server**: HTTP testing without network
Integration tests cover:
- Space claiming workflow
- Subdomain availability checking
- Profanity filtering
- Custom DWS/Tanishq Dubey trademark filtering
- DynDNS2 protocol updates
- Full end-to-end workflows
### Test Files
```
internal/config/config_test.go # Config tests
internal/database/db_test.go # Database tests
internal/handlers/handlers_test.go # Handler unit tests
internal/handlers/validation.go # Custom validators
internal/testutil/mock_technitium.go # Mock DNS server
tests/integration/integration_test.go # Integration tests
```
### Running Integration Tests
```bash
# Run all integration tests
go test ./tests/integration -v
# Run specific integration test
go test ./tests/integration -v -run TestIntegration_FullWorkflow
# Run with timeout
go test ./tests/integration -v -timeout 30s
# Run containerized integration tests with podman/docker-compose
make test-compose
# Or using the shell script directly
bash tests/integration/compose_test.sh
# Run Go-based compose tests (requires RUN_COMPOSE_TESTS=true)
RUN_COMPOSE_TESTS=true go test ./tests/integration -v -run TestComposeIntegration
```
### Using Make
```bash
make help # Show all available targets
make build # Build the binary
make test # Run all tests
make test-unit # Run unit tests only
make test-integration # Run integration tests
make test-compose # Run containerized tests
make run # Build and run the server
make dev # Run in development mode
``` ```
## License ## License

6
go.mod
View File

@@ -3,15 +3,17 @@ module git.dws.rip/DWS/dyn
go 1.24.4 go 1.24.4
require ( require (
github.com/TwiN/go-away v1.8.1
github.com/gin-gonic/gin v1.11.0 github.com/gin-gonic/gin v1.11.0
github.com/mattn/go-sqlite3 v1.14.24 github.com/mattn/go-sqlite3 v1.14.24
github.com/stretchr/testify v1.11.1
) )
require ( require (
github.com/TwiN/go-away v1.8.1 // indirect
github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect github.com/cloudwego/base64x v0.1.6 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
@@ -26,6 +28,7 @@ require (
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect github.com/quic-go/quic-go v0.54.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
@@ -40,4 +43,5 @@ require (
golang.org/x/text v0.30.0 // indirect golang.org/x/text v0.30.0 // indirect
golang.org/x/tools v0.37.0 // indirect golang.org/x/tools v0.37.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect google.golang.org/protobuf v1.36.9 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

15
go.sum
View File

@@ -69,37 +69,24 @@ go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -0,0 +1,372 @@
package config
import (
"os"
"testing"
)
func TestLoad(t *testing.T) {
// Clean up any existing env vars
cleanup := cleanEnv()
defer cleanup()
// Set test values
os.Setenv("TECHNITIUM_URL", "https://test.dns.example.com")
os.Setenv("TECHNITIUM_TOKEN", "test-token-12345")
os.Setenv("BASE_DOMAIN", "test.rip")
os.Setenv("SPACE_SUBDOMAIN", "dyn")
os.Setenv("RATE_LIMIT_PER_IP", "20")
os.Setenv("RATE_LIMIT_PER_TOKEN", "5")
cfg := Load()
tests := []struct {
name string
got string
expected string
}{
{"TechnitiumURL", cfg.TechnitiumURL, "https://test.dns.example.com"},
{"TechnitiumToken", cfg.TechnitiumToken, "test-token-12345"},
{"BaseDomain", cfg.BaseDomain, "test.rip"},
{"SpaceSubdomain", cfg.SpaceSubdomain, "dyn"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.got != tt.expected {
t.Errorf("%s = %v, want %v", tt.name, tt.got, tt.expected)
}
})
}
// Test numeric values
if cfg.RateLimitPerIP != 20 {
t.Errorf("RateLimitPerIP = %v, want 20", cfg.RateLimitPerIP)
}
if cfg.RateLimitPerToken != 5 {
t.Errorf("RateLimitPerToken = %v, want 5", cfg.RateLimitPerToken)
}
}
func TestLoadDefaults(t *testing.T) {
cleanup := cleanEnv()
defer cleanup()
cfg := Load()
tests := []struct {
name string
got string
expected string
}{
{"ServerPort", cfg.ServerPort, "8080"},
{"DatabasePath", cfg.DatabasePath, "./dyn.db"},
{"BaseDomain", cfg.BaseDomain, "dws.rip"},
{"SpaceSubdomain", cfg.SpaceSubdomain, "space"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.got != tt.expected {
t.Errorf("%s = %v, want %v", tt.name, tt.got, tt.expected)
}
})
}
// Test default numeric values
if cfg.RateLimitPerIP != 10 {
t.Errorf("RateLimitPerIP default = %v, want 10", cfg.RateLimitPerIP)
}
if cfg.RateLimitPerToken != 1 {
t.Errorf("RateLimitPerToken default = %v, want 1", cfg.RateLimitPerToken)
}
}
func TestValidate(t *testing.T) {
tests := []struct {
name string
setupEnv func()
wantErrors int
}{
{
name: "valid config with token",
setupEnv: func() {
os.Setenv("TECHNITIUM_URL", "https://dns.example.com")
os.Setenv("TECHNITIUM_TOKEN", "valid-token")
},
wantErrors: 0,
},
{
name: "valid config with username/password",
setupEnv: func() {
os.Setenv("TECHNITIUM_URL", "https://dns.example.com")
os.Setenv("TECHNITIUM_USERNAME", "admin")
os.Setenv("TECHNITIUM_PASSWORD", "secret")
os.Unsetenv("TECHNITIUM_TOKEN")
},
wantErrors: 0,
},
{
name: "missing url",
setupEnv: func() {
os.Unsetenv("TECHNITIUM_URL")
os.Setenv("TECHNITIUM_TOKEN", "token")
},
wantErrors: 1,
},
{
name: "missing auth",
setupEnv: func() {
os.Setenv("TECHNITIUM_URL", "https://dns.example.com")
os.Unsetenv("TECHNITIUM_TOKEN")
os.Unsetenv("TECHNITIUM_USERNAME")
os.Unsetenv("TECHNITIUM_PASSWORD")
},
wantErrors: 1,
},
{
name: "missing everything",
setupEnv: func() {
os.Unsetenv("TECHNITIUM_URL")
os.Unsetenv("TECHNITIUM_TOKEN")
os.Unsetenv("TECHNITIUM_USERNAME")
os.Unsetenv("TECHNITIUM_PASSWORD")
},
wantErrors: 2,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cleanup := cleanEnv()
defer cleanup()
tt.setupEnv()
cfg := Load()
errors := cfg.Validate()
if len(errors) != tt.wantErrors {
t.Errorf("Validate() returned %d errors, want %d: %v", len(errors), tt.wantErrors, errors)
}
})
}
}
func TestGetZone(t *testing.T) {
tests := []struct {
name string
base string
space string
expected string
}{
{
name: "with space subdomain",
base: "dws.rip",
space: "space",
expected: "space.dws.rip",
},
{
name: "without space subdomain",
base: "dws.rip",
space: "",
expected: "dws.rip",
},
{
name: "nested subdomain",
base: "example.com",
space: "dyn.space",
expected: "dyn.space.example.com",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &Config{
BaseDomain: tt.base,
SpaceSubdomain: tt.space,
}
got := cfg.GetZone()
if got != tt.expected {
t.Errorf("GetZone() = %v, want %v", got, tt.expected)
}
})
}
}
func TestGetEnv(t *testing.T) {
tests := []struct {
name string
key string
value string
defaultValue string
expected string
}{
{
name: "env set",
key: "TEST_KEY",
value: "test-value",
defaultValue: "default",
expected: "test-value",
},
{
name: "env not set",
key: "UNSET_TEST_KEY",
value: "",
defaultValue: "default",
expected: "default",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.value != "" {
os.Setenv(tt.key, tt.value)
defer os.Unsetenv(tt.key)
}
got := getEnv(tt.key, tt.defaultValue)
if got != tt.expected {
t.Errorf("getEnv() = %v, want %v", got, tt.expected)
}
})
}
}
func TestGetEnvAsInt(t *testing.T) {
tests := []struct {
name string
value string
defaultValue int
expected int
}{
{
name: "valid int",
value: "42",
defaultValue: 10,
expected: 42,
},
{
name: "invalid int",
value: "not-a-number",
defaultValue: 10,
expected: 10,
},
{
name: "empty value",
value: "",
defaultValue: 10,
expected: 10,
},
{
name: "negative int",
value: "-5",
defaultValue: 10,
expected: -5,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
key := "TEST_INT_KEY"
if tt.value != "" {
os.Setenv(key, tt.value)
defer os.Unsetenv(key)
}
got := getEnvAsInt(key, tt.defaultValue)
if got != tt.expected {
t.Errorf("getEnvAsInt() = %v, want %v", got, tt.expected)
}
})
}
}
func TestGetEnvAsSlice(t *testing.T) {
tests := []struct {
name string
value string
defaultValue []string
expected []string
}{
{
name: "single value",
value: "10.0.0.0/8",
defaultValue: []string{},
expected: []string{"10.0.0.0/8"},
},
{
name: "multiple values",
value: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16",
defaultValue: []string{},
expected: []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"},
},
{
name: "empty value",
value: "",
defaultValue: []string{"default"},
expected: []string{"default"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
key := "TEST_SLICE_KEY"
if tt.value != "" {
os.Setenv(key, tt.value)
defer os.Unsetenv(key)
}
got := getEnvAsSlice(key, tt.defaultValue)
if len(got) != len(tt.expected) {
t.Errorf("getEnvAsSlice() length = %v, want %v", len(got), len(tt.expected))
return
}
for i, v := range got {
if v != tt.expected[i] {
t.Errorf("getEnvAsSlice()[%d] = %v, want %v", i, v, tt.expected[i])
}
}
})
}
}
// cleanEnv removes all DYN-related env vars and returns a cleanup function
func cleanEnv() func() {
vars := []string{
"SERVER_PORT",
"DATABASE_PATH",
"TECHNITIUM_URL",
"TECHNITIUM_USERNAME",
"TECHNITIUM_PASSWORD",
"TECHNITIUM_TOKEN",
"BASE_DOMAIN",
"SPACE_SUBDOMAIN",
"RATE_LIMIT_PER_IP",
"RATE_LIMIT_PER_TOKEN",
"TRUSTED_PROXIES",
}
// Store old values
oldValues := make(map[string]string)
for _, v := range vars {
if val, ok := os.LookupEnv(v); ok {
oldValues[v] = val
os.Unsetenv(v)
}
}
// Return cleanup function
return func() {
for _, v := range vars {
os.Unsetenv(v)
}
for v, val := range oldValues {
os.Setenv(v, val)
}
}
}

View File

@@ -0,0 +1,353 @@
package database
import (
"context"
"os"
"testing"
"time"
"git.dws.rip/DWS/dyn/internal/models"
)
func setupTestDB(t *testing.T) (*DB, func()) {
tmpFile := "/tmp/test_dyn_" + time.Now().Format("20060102150405") + ".db"
db, err := New(tmpFile)
if err != nil {
t.Fatalf("Failed to create test database: %v", err)
}
cleanup := func() {
db.Close()
os.Remove(tmpFile)
}
return db, cleanup
}
func TestCreateSpace(t *testing.T) {
db, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
tests := []struct {
name string
subdomain string
wantErr bool
}{
{
name: "create valid space",
subdomain: "myhome",
wantErr: false,
},
{
name: "create another valid space",
subdomain: "office",
wantErr: false,
},
{
name: "duplicate subdomain",
subdomain: "myhome",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
space, err := db.CreateSpace(ctx, tt.subdomain)
if tt.wantErr {
if err == nil {
t.Errorf("CreateSpace() expected error but got none")
}
return
}
if err != nil {
t.Errorf("CreateSpace() unexpected error: %v", err)
return
}
if space == nil {
t.Errorf("CreateSpace() returned nil space")
return
}
if space.Subdomain != tt.subdomain {
t.Errorf("CreateSpace() subdomain = %v, want %v", space.Subdomain, tt.subdomain)
}
if space.Token == "" {
t.Errorf("CreateSpace() token is empty")
}
})
}
}
func TestGetSpaceByToken(t *testing.T) {
db, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Create a test space
created, err := db.CreateSpace(ctx, "testspace")
if err != nil {
t.Fatalf("Failed to create test space: %v", err)
}
tests := []struct {
name string
token string
wantNil bool
}{
{
name: "existing space",
token: created.Token,
wantNil: false,
},
{
name: "non-existent token",
token: "invalid-token-12345",
wantNil: true,
},
{
name: "empty token",
token: "",
wantNil: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
space, err := db.GetSpaceByToken(ctx, tt.token)
if err != nil {
t.Errorf("GetSpaceByToken() unexpected error: %v", err)
return
}
if tt.wantNil && space != nil {
t.Errorf("GetSpaceByToken() expected nil, got %v", space)
}
if !tt.wantNil && space == nil {
t.Errorf("GetSpaceByToken() expected space, got nil")
}
})
}
}
func TestGetSpaceBySubdomain(t *testing.T) {
db, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Create a test space
created, err := db.CreateSpace(ctx, "mytestspace")
if err != nil {
t.Fatalf("Failed to create test space: %v", err)
}
tests := []struct {
name string
subdomain string
wantNil bool
}{
{
name: "existing subdomain",
subdomain: "mytestspace",
wantNil: false,
},
{
name: "non-existent subdomain",
subdomain: "nonexistent",
wantNil: true,
},
{
name: "case sensitivity test",
subdomain: "MyTestSpace",
wantNil: true, // SQLite is case-sensitive by default
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
space, err := db.GetSpaceBySubdomain(ctx, tt.subdomain)
if err != nil {
t.Errorf("GetSpaceBySubdomain() unexpected error: %v", err)
return
}
if tt.wantNil && space != nil {
t.Errorf("GetSpaceBySubdomain() expected nil, got %v", space)
}
if !tt.wantNil && space == nil {
t.Errorf("GetSpaceBySubdomain() expected space, got nil")
}
if !tt.wantNil && space.Token != created.Token {
t.Errorf("GetSpaceBySubdomain() token mismatch")
}
})
}
}
func TestUpdateSpaceIP(t *testing.T) {
db, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Create a test space
created, err := db.CreateSpace(ctx, "updatespace")
if err != nil {
t.Fatalf("Failed to create test space: %v", err)
}
tests := []struct {
name string
token string
ip string
wantErr bool
}{
{
name: "update valid space",
token: created.Token,
ip: "192.168.1.100",
wantErr: false,
},
{
name: "update with different IP",
token: created.Token,
ip: "10.0.0.50",
wantErr: false,
},
{
name: "update non-existent space",
token: "invalid-token",
ip: "192.168.1.1",
wantErr: false, // SQL UPDATE with no match doesn't error
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := db.UpdateSpaceIP(ctx, tt.token, tt.ip)
if tt.wantErr && err == nil {
t.Errorf("UpdateSpaceIP() expected error but got none")
}
if !tt.wantErr && err != nil {
t.Errorf("UpdateSpaceIP() unexpected error: %v", err)
}
// Verify the update for valid token
if !tt.wantErr && tt.token == created.Token {
space, err := db.GetSpaceByToken(ctx, tt.token)
if err != nil {
t.Errorf("Failed to get space after update: %v", err)
return
}
if space.LastIP != tt.ip {
t.Errorf("UpdateSpaceIP() IP = %v, want %v", space.LastIP, tt.ip)
}
}
})
}
}
func TestSubdomainExists(t *testing.T) {
db, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Create a test space
_, err := db.CreateSpace(ctx, "existingspace")
if err != nil {
t.Fatalf("Failed to create test space: %v", err)
}
tests := []struct {
name string
subdomain string
want bool
}{
{
name: "existing subdomain",
subdomain: "existingspace",
want: true,
},
{
name: "non-existent subdomain",
subdomain: "newspace",
want: false,
},
{
name: "empty subdomain",
subdomain: "",
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
exists, err := db.SubdomainExists(ctx, tt.subdomain)
if err != nil {
t.Errorf("SubdomainExists() unexpected error: %v", err)
return
}
if exists != tt.want {
t.Errorf("SubdomainExists() = %v, want %v", exists, tt.want)
}
})
}
}
func TestTokenGeneration(t *testing.T) {
tokens := make(map[string]bool)
// Generate 100 tokens and ensure they're all unique
for i := 0; i < 100; i++ {
token, err := generateToken()
if err != nil {
t.Fatalf("generateToken() error: %v", err)
}
if token == "" {
t.Errorf("generateToken() returned empty string")
}
if len(token) < 16 {
t.Errorf("generateToken() returned short token: %d chars", len(token))
}
if tokens[token] {
t.Errorf("generateToken() returned duplicate token: %s", token)
}
tokens[token] = true
}
}
func TestSpaceModel(t *testing.T) {
space := &models.Space{
Token: "test-token",
Subdomain: "myspace",
LastIP: "192.168.1.1",
}
fqdn := space.GetFQDN("space.dws.rip")
if fqdn != "myspace.space.dws.rip" {
t.Errorf("GetFQDN() = %v, want %v", fqdn, "myspace.space.dws.rip")
}
}

View File

@@ -0,0 +1,243 @@
package handlers
import (
"testing"
)
func TestCustomFilter_IsBlocked(t *testing.T) {
cf := NewCustomFilter()
tests := []struct {
name string
subdomain string
want bool
}{
{
name: "exact match - dws",
subdomain: "dws",
want: true,
},
{
name: "exact match - dubey",
subdomain: "dubey",
want: true,
},
{
name: "exact match - tanishq",
subdomain: "tanishq",
want: true,
},
{
name: "exact match - tdubey",
subdomain: "tdubey",
want: true,
},
{
name: "with hyphens - dubey-web",
subdomain: "dubey-web",
want: true,
},
{
name: "with hyphens - tanishq-dubey",
subdomain: "tanishq-dubey",
want: true,
},
{
name: "leet speak - dub3y",
subdomain: "dub3y",
want: true,
},
{
name: "leet speak - t4nishq",
subdomain: "t4nishq",
want: true,
},
{
name: "leet speak - dw5",
subdomain: "dw5",
want: true,
},
{
name: "combined term - dubeydns",
subdomain: "dubeydns",
want: true,
},
{
name: "combined term - dws-ddns",
subdomain: "dws-ddns",
want: true,
},
{
name: "safe subdomain - myhome",
subdomain: "myhome",
want: false,
},
{
name: "safe subdomain - office",
subdomain: "office",
want: false,
},
{
name: "safe subdomain - server01",
subdomain: "server01",
want: false,
},
{
name: "case insensitive - DWS",
subdomain: "DWS",
want: true,
},
{
name: "case insensitive - Tanishq",
subdomain: "Tanishq",
want: true,
},
{
name: "mixed case - DuBeY",
subdomain: "DuBeY",
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := cf.IsBlocked(tt.subdomain)
if got != tt.want {
t.Errorf("IsBlocked(%q) = %v, want %v", tt.subdomain, got, tt.want)
}
})
}
}
func TestCustomFilter_normalize(t *testing.T) {
cf := NewCustomFilter()
tests := []struct {
name string
text string
want string
}{
{
name: "lowercase conversion",
text: "DuBeY",
want: "dubey",
},
{
name: "remove hyphens",
text: "dubey-web",
want: "dubeyweb",
},
{
name: "remove underscores",
text: "dubey_web",
want: "dubeyweb",
},
{
name: "remove dots",
text: "dubey.web",
want: "dubeyweb",
},
{
name: "remove spaces",
text: "dubey web",
want: "dubeyweb",
},
{
name: "leet speak conversion",
text: "dub3y",
want: "dubey",
},
{
name: "leet speak conversion - t4nishq",
text: "t4nishq",
want: "tanishq",
},
{
name: "leet speak conversion - dw5",
text: "dw5",
want: "dws",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := cf.normalize(tt.text)
if got != tt.want {
t.Errorf("normalize(%q) = %q, want %q", tt.text, got, tt.want)
}
})
}
}
func TestIsValidSubdomain(t *testing.T) {
tests := []struct {
name string
subdomain string
want bool
}{
{
name: "valid - simple",
subdomain: "myhome",
want: true,
},
{
name: "valid - with hyphen",
subdomain: "my-home",
want: true,
},
{
name: "valid - with numbers",
subdomain: "home123",
want: true,
},
{
name: "valid - minimum length",
subdomain: "abc",
want: true,
},
{
name: "invalid - too short",
subdomain: "ab",
want: false,
},
{
name: "invalid - too long",
subdomain: "thisisaverylongsubdomainthatexceedsthesixtythreecharacterlimitforsubdomains",
want: false,
},
{
name: "invalid - starts with hyphen",
subdomain: "-myhome",
want: false,
},
{
name: "invalid - ends with hyphen",
subdomain: "myhome-",
want: false,
},
{
name: "invalid - contains special chars",
subdomain: "my_home",
want: false,
},
{
name: "invalid - contains dot",
subdomain: "my.home",
want: false,
},
{
name: "invalid - empty",
subdomain: "",
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := isValidSubdomain(tt.subdomain)
if got != tt.want {
t.Errorf("isValidSubdomain(%q) = %v, want %v", tt.subdomain, got, tt.want)
}
})
}
}

View File

@@ -0,0 +1,45 @@
package handlers
import (
"regexp"
"strings"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
)
func init() {
// Register custom validators
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("alphanumdash", alphanumdashValidator)
}
}
// alphanumdashValidator validates that a string contains only alphanumeric characters and hyphens
var alphanumdashValidator validator.Func = func(fl validator.FieldLevel) bool {
value := fl.Field().String()
// Allow alphanumeric and hyphens only
match := regexp.MustCompile(`^[a-zA-Z0-9-]+$`).MatchString(value)
return match
}
// IsValidSubdomainFormat checks if a subdomain string is valid (exported for use in validation)
func IsValidSubdomainFormat(subdomain string) bool {
// Must be at least 3 characters
if len(subdomain) < 3 {
return false
}
// Must not exceed 63 characters
if len(subdomain) > 63 {
return false
}
// Must not start or end with hyphen
if strings.HasPrefix(subdomain, "-") || strings.HasSuffix(subdomain, "-") {
return false
}
// Must contain only alphanumeric and hyphens
return regexp.MustCompile(`^[a-zA-Z0-9-]+$`).MatchString(subdomain)
}

View File

@@ -0,0 +1,238 @@
package testutil
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"sync"
)
// MockTechnitiumServer simulates the Technitium DNS API for testing
type MockTechnitiumServer struct {
Server *httptest.Server
Records map[string]MockDNSRecord
mu sync.RWMutex
Username string
Password string
Token string
}
// MockDNSRecord represents a DNS record stored in the mock server
type MockDNSRecord struct {
Domain string `json:"domain"`
Type string `json:"type"`
IPAddress string `json:"ipAddress"`
TTL int `json:"ttl"`
}
// NewMockTechnitiumServer creates a new mock Technitium server
func NewMockTechnitiumServer() *MockTechnitiumServer {
mock := &MockTechnitiumServer{
Records: make(map[string]MockDNSRecord),
Username: "admin",
Password: "test-password",
Token: "test-api-token",
}
mux := http.NewServeMux()
mux.HandleFunc("/api/dns/records/add", mock.handleAddRecord)
mux.HandleFunc("/api/dns/records/delete", mock.handleDeleteRecord)
mux.HandleFunc("/api/dns/records/get", mock.handleGetRecords)
// Health check endpoint
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("Technitium DNS Server Mock"))
})
mock.Server = httptest.NewServer(mux)
return mock
}
// Close shuts down the mock server
func (m *MockTechnitiumServer) Close() {
m.Server.Close()
}
// URL returns the base URL of the mock server
func (m *MockTechnitiumServer) URL() string {
return m.Server.URL
}
// GetRecords returns all stored DNS records (for testing assertions)
func (m *MockTechnitiumServer) GetRecords() map[string]MockDNSRecord {
m.mu.RLock()
defer m.mu.RUnlock()
// Return a copy to avoid race conditions
records := make(map[string]MockDNSRecord)
for k, v := range m.Records {
records[k] = v
}
return records
}
// GetRecordCount returns the number of stored records
func (m *MockTechnitiumServer) GetRecordCount() int {
m.mu.RLock()
defer m.mu.RUnlock()
return len(m.Records)
}
// ClearRecords removes all stored records
func (m *MockTechnitiumServer) ClearRecords() {
m.mu.Lock()
defer m.mu.Unlock()
m.Records = make(map[string]MockDNSRecord)
}
func (m *MockTechnitiumServer) authenticate(r *http.Request) bool {
// Check for API token in header
authHeader := r.Header.Get("Authorization")
if authHeader == "Basic "+m.Token {
return true
}
// Check for username/password in basic auth
user, pass, ok := r.BasicAuth()
if ok && user == m.Username && pass == m.Password {
return true
}
return false
}
func (m *MockTechnitiumServer) handleAddRecord(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
if !m.authenticate(r) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "error",
"error": map[string]string{
"code": "Unauthorized",
"message": "Invalid credentials",
},
})
return
}
// Parse form data
if err := r.ParseForm(); err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
domain := r.FormValue("domain")
recordType := r.FormValue("type")
ipAddress := r.FormValue("ipAddress")
if domain == "" || recordType == "" {
http.Error(w, "Missing required fields", http.StatusBadRequest)
return
}
// Store the record
m.mu.Lock()
key := fmt.Sprintf("%s:%s", domain, recordType)
m.Records[key] = MockDNSRecord{
Domain: domain,
Type: recordType,
IPAddress: ipAddress,
TTL: 300,
}
m.mu.Unlock()
// Return success response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "ok",
"response": map[string]interface{}{
"domain": domain,
"type": recordType,
},
})
}
func (m *MockTechnitiumServer) handleDeleteRecord(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
if !m.authenticate(r) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "error",
"error": map[string]string{
"code": "Unauthorized",
"message": "Invalid credentials",
},
})
return
}
// Parse form data
if err := r.ParseForm(); err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
domain := r.FormValue("domain")
recordType := r.FormValue("type")
// Delete the record
m.mu.Lock()
key := fmt.Sprintf("%s:%s", domain, recordType)
delete(m.Records, key)
m.mu.Unlock()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "ok",
})
}
func (m *MockTechnitiumServer) handleGetRecords(w http.ResponseWriter, r *http.Request) {
if !m.authenticate(r) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "error",
"error": map[string]string{
"code": "Unauthorized",
"message": "Invalid credentials",
},
})
return
}
domain := r.URL.Query().Get("domain")
m.mu.RLock()
var records []MockDNSRecord
for _, record := range m.Records {
if domain == "" || record.Domain == domain {
records = append(records, record)
}
}
m.mu.RUnlock()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "ok",
"response": map[string]interface{}{
"domain": domain,
"records": records,
},
})
}

View File

@@ -0,0 +1,234 @@
package integration
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"os"
"os/exec"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// ComposeIntegrationTest runs integration tests against a containerized instance
func TestComposeIntegration(t *testing.T) {
if os.Getenv("RUN_COMPOSE_TESTS") != "true" {
t.Skip("Skipping compose integration tests. Set RUN_COMPOSE_TESTS=true to run")
}
composeCmd := detectComposeCommand(t)
projectName := "dyn-test-" + fmt.Sprintf("%d", time.Now().Unix())
// Cleanup function
cleanup := func() {
t.Log("Cleaning up containers...")
cmd := exec.Command(composeCmd, "-f", "../../docker-compose.yml", "-p", projectName, "down", "-v")
cmd.Run()
os.Remove(".env.test")
}
defer cleanup()
// Create test environment
envContent := `SERVER_PORT=8080
DATABASE_PATH=/data/dyn.db
TECHNITIUM_URL=http://mock-dns:8080
TECHNITIUM_TOKEN=test-token
BASE_DOMAIN=test.rip
SPACE_SUBDOMAIN=space
RATE_LIMIT_PER_IP=100
RATE_LIMIT_PER_TOKEN=100
`
err := os.WriteFile(".env.test", []byte(envContent), 0644)
require.NoError(t, err)
// Start services
t.Log("Starting services with", composeCmd)
cmd := exec.Command(composeCmd, "-f", "../../docker-compose.yml", "-p", projectName, "--env-file", ".env.test", "up", "-d", "--build")
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("Failed to start services: %v\nOutput: %s", err, output)
}
// Wait for service to be ready
baseURL := waitForService(t, "http://localhost:8080", 60)
t.Logf("Service ready at %s", baseURL)
// Run tests
t.Run("HealthCheck", func(t *testing.T) {
testHealthCheck(t, baseURL)
})
t.Run("CheckSubdomain", func(t *testing.T) {
testCheckSubdomain(t, baseURL)
})
t.Run("ClaimSpace", func(t *testing.T) {
testClaimSpace(t, baseURL)
})
t.Run("ProfanityFilter", func(t *testing.T) {
testProfanityFilterCompose(t, baseURL)
})
t.Run("CustomFilter", func(t *testing.T) {
testCustomFilterCompose(t, baseURL)
})
t.Run("DynDNSEndpoint", func(t *testing.T) {
testDynDNSEndpoint(t, baseURL)
})
}
func detectComposeCommand(t *testing.T) string {
// Try podman-compose first
if _, err := exec.LookPath("podman-compose"); err == nil {
t.Log("Using podman-compose")
return "podman-compose"
}
// Try docker-compose
if _, err := exec.LookPath("docker-compose"); err == nil {
t.Log("Using docker-compose")
return "docker-compose"
}
t.Skip("Neither podman-compose nor docker-compose found")
return ""
}
func waitForService(t *testing.T, url string, timeout int) string {
client := &http.Client{Timeout: 2 * time.Second}
for i := 0; i < timeout; i++ {
resp, err := client.Get(url)
if err == nil && resp.StatusCode == http.StatusOK {
resp.Body.Close()
return url
}
if resp != nil {
resp.Body.Close()
}
time.Sleep(1 * time.Second)
}
t.Fatalf("Service did not become ready within %d seconds", timeout)
return ""
}
func testHealthCheck(t *testing.T, baseURL string) {
resp, err := http.Get(baseURL + "/")
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
body := new(bytes.Buffer)
body.ReadFrom(resp.Body)
assert.Contains(t, body.String(), "DWS Dynamic DNS")
}
func testCheckSubdomain(t *testing.T, baseURL string) {
// Test available subdomain
resp, err := http.Get(baseURL + "/api/check?subdomain=newspace")
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var result map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.True(t, result["available"].(bool))
}
func testClaimSpace(t *testing.T, baseURL string) string {
payload := map[string]string{"subdomain": "testclaim"}
body, _ := json.Marshal(payload)
resp, err := http.Post(baseURL+"/api/claim", "application/json", bytes.NewBuffer(body))
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusCreated, resp.StatusCode)
var result map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
token := result["token"].(string)
assert.NotEmpty(t, token)
assert.Equal(t, "testclaim", result["subdomain"])
return token
}
func testProfanityFilterCompose(t *testing.T, baseURL string) {
profaneWords := []string{"fuck", "shit", "bitch"}
for _, word := range profaneWords {
t.Run(word, func(t *testing.T) {
payload := map[string]string{"subdomain": word}
body, _ := json.Marshal(payload)
resp, err := http.Post(baseURL+"/api/claim", "application/json", bytes.NewBuffer(body))
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
var result map[string]string
json.NewDecoder(resp.Body).Decode(&result)
assert.Contains(t, result["error"], "inappropriate")
})
}
}
func testCustomFilterCompose(t *testing.T, baseURL string) {
reservedWords := []string{"dws", "dubey", "tanishq"}
for _, word := range reservedWords {
t.Run(word, func(t *testing.T) {
payload := map[string]string{"subdomain": word}
body, _ := json.Marshal(payload)
resp, err := http.Post(baseURL+"/api/claim", "application/json", bytes.NewBuffer(body))
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
var result map[string]string
json.NewDecoder(resp.Body).Decode(&result)
assert.Contains(t, result["error"], "reserved")
})
}
}
func testDynDNSEndpoint(t *testing.T, baseURL string) {
// First claim a space
token := testClaimSpace(t, baseURL)
// Try DynDNS update
client := &http.Client{Timeout: 5 * time.Second}
req, err := http.NewRequest("GET", baseURL+"/api/nic/update?hostname=testclaim.space.test.rip&myip=192.168.1.100", nil)
require.NoError(t, err)
req.Header.Set("Authorization", "Basic "+basicAuth("none", token))
resp, err := client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
// We expect 200 or 503 (since there's no real DNS server)
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusServiceUnavailable)
}
func basicAuth(username, password string) string {
auth := username + ":" + password
return base64.StdEncoding.EncodeToString([]byte(auth))
}

282
tests/integration/compose_test.sh Executable file
View File

@@ -0,0 +1,282 @@
#!/bin/bash
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
COMPOSE_FILE="docker-compose.yml"
TEST_TIMEOUT=60
CONTAINER_NAME="dyn-ddns-test"
VERBOSE=false
# Generate unique test identifiers
TEST_ID=$(date +%s%N | tail -c 8)
TEST_SUBDOMAIN="test${TEST_ID}"
CLAIM_SUBDOMAIN="claim${TEST_ID}"
PROFANE_TEST="profane${TEST_ID}"
RESERVED_TEST="reserved${TEST_ID}"
INVALID_TEST="ab${TEST_ID}"
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-v|--verbose)
VERBOSE=true
shift
;;
*)
echo "Unknown option: $1"
echo "Usage: $0 [-v|--verbose]"
exit 1
;;
esac
done
# Verbose logging function
log_verbose() {
if [ "$VERBOSE" = true ]; then
echo -e "${BLUE}[VERBOSE] $1${NC}"
fi
}
dump_logs() {
if [ "$VERBOSE" = true ]; then
echo -e "${YELLOW}=== Container Logs ===${NC}"
$COMPOSE_CMD -f $COMPOSE_FILE -p $CONTAINER_NAME logs --tail=50 || true
echo -e "${YELLOW}=== End Logs ===${NC}"
fi
}
echo -e "${YELLOW}=== DDNS Container Integration Test ===${NC}"
if [ "$VERBOSE" = true ]; then
echo -e "${BLUE}Verbose mode enabled${NC}"
fi
# Detect compose command
if command -v podman-compose &> /dev/null; then
COMPOSE_CMD="podman-compose"
echo -e "${GREEN}Using podman-compose${NC}"
elif command -v docker-compose &> /dev/null; then
COMPOSE_CMD="docker-compose"
echo -e "${GREEN}Using docker-compose${NC}"
else
echo -e "${RED}Error: Neither podman-compose nor docker-compose found${NC}"
exit 1
fi
# Cleanup function
cleanup() {
echo -e "${YELLOW}Cleaning up...${NC}"
dump_logs
$COMPOSE_CMD -f $COMPOSE_FILE -p $CONTAINER_NAME down -v 2>/dev/null || true
rm -f .env.test
}
# Set trap for cleanup on exit
trap cleanup EXIT
# Create test environment file
cat > .env.test << EOF
SERVER_PORT=8080
DATABASE_PATH=/data/dyn.db
TECHNITIUM_URL=http://mock-dns:8080
TECHNITIUM_TOKEN=test-token
BASE_DOMAIN=test.rip
SPACE_SUBDOMAIN=space
RATE_LIMIT_PER_IP=100
RATE_LIMIT_PER_TOKEN=100
EOF
log_verbose "Environment file created:"
log_verbose "$(cat .env.test)"
echo -e "${YELLOW}Starting services...${NC}"
$COMPOSE_CMD -f $COMPOSE_FILE -p $CONTAINER_NAME --env-file .env.test up -d --build --renew-anon-volumes 2>&1 | tee /tmp/compose-build.log
log_verbose "Build log saved to /tmp/compose-build.log"
# Wait for services to be ready
echo -e "${YELLOW}Waiting for services to start (timeout: ${TEST_TIMEOUT}s)...${NC}"
for i in $(seq 1 $TEST_TIMEOUT); do
if curl -s http://localhost:8080/ > /dev/null 2>&1; then
echo -e "${GREEN}Services are ready!${NC}"
break
fi
if [ $i -eq $TEST_TIMEOUT ]; then
echo -e "${RED}Timeout waiting for services${NC}"
dump_logs
exit 1
fi
sleep 1
done
echo -e "${YELLOW}Running integration tests...${NC}"
# Test 1: Health check
echo -n "Test 1: Health check... "
log_verbose "Running: curl -v http://localhost:8080/"
if [ "$VERBOSE" = true ]; then
HTTP_RESPONSE=$(curl -v http://localhost:8080/ 2>&1)
echo "$HTTP_RESPONSE" | tee /tmp/test1.log
else
HTTP_RESPONSE=$(curl -s http://localhost:8080/)
fi
if echo "$HTTP_RESPONSE" | grep -q "DWS Dynamic DNS"; then
echo -e "${GREEN}PASS${NC}"
else
echo -e "${RED}FAIL${NC}"
log_verbose "Response: $HTTP_RESPONSE"
exit 1
fi
# Test 2: Check subdomain availability
echo -n "Test 2: Check subdomain availability... "
log_verbose "Running: curl -v http://localhost:8080/api/check?subdomain=testspace"
if [ "$VERBOSE" = true ]; then
RESPONSE=$(curl -v "http://localhost:8080/api/check?subdomain=testspace" 2>&1)
echo "$RESPONSE" | tee /tmp/test2.log
else
RESPONSE=$(curl -s "http://localhost:8080/api/check?subdomain=testspace")
fi
log_verbose "Response: $RESPONSE"
if echo "$RESPONSE" | grep -q '"available":true'; then
echo -e "${GREEN}PASS${NC}"
else
echo -e "${RED}FAIL${NC} - Response: $RESPONSE"
exit 1
fi
# Test 3: Claim space
echo -n "Test 3: Claim space... "
log_verbose "Running: curl -v -X POST http://localhost:8080/api/claim"
if [ "$VERBOSE" = true ]; then
RESPONSE=$(curl -v -X POST http://localhost:8080/api/claim \
-H "Content-Type: application/json" \
-d "{\"subdomain\":\"$CLAIM_SUBDOMAIN\"}" 2>&1)
echo "$RESPONSE" | tee /tmp/test3.log
else
RESPONSE=$(curl -s -X POST http://localhost:8080/api/claim \
-H "Content-Type: application/json" \
-d "{\"subdomain\":\"$CLAIM_SUBDOMAIN\"}")
fi
log_verbose "Response: $RESPONSE"
# Check for HTTP 201 Created status and valid token format
if echo "$RESPONSE" | grep -q 'HTTP/1.1 201 Created' && echo "$RESPONSE" | grep -q '"token":"[A-Za-z0-9_-]\+"'; then
TOKEN=$(echo "$RESPONSE" | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
echo -e "${GREEN}PASS${NC} (token: ${TOKEN:0:20}...)"
log_verbose "Full token: $TOKEN"
else
echo -e "${RED}FAIL${NC} - Response: $RESPONSE"
exit 1
fi
# Test 4: Check claimed subdomain is not available
echo -n "Test 4: Check claimed subdomain unavailable... "
log_verbose "Running: curl -v http://localhost:8080/api/check?subdomain=$CLAIM_SUBDOMAIN"
if [ "$VERBOSE" = true ]; then
RESPONSE=$(curl -v "http://localhost:8080/api/check?subdomain=$CLAIM_SUBDOMAIN" 2>&1)
echo "$RESPONSE" | tee /tmp/test4.log
else
RESPONSE=$(curl -s "http://localhost:8080/api/check?subdomain=$CLAIM_SUBDOMAIN")
fi
log_verbose "Response: $RESPONSE"
if echo "$RESPONSE" | grep -q '"available":false'; then
echo -e "${GREEN}PASS${NC}"
else
echo -e "${RED}FAIL${NC} - Response: $RESPONSE"
exit 1
fi
# Test 5: DynDNS update (mocked - will fail DNS but test auth)
echo -n "Test 5: DynDNS update endpoint... "
log_verbose "Running: curl -v -u none:<token> http://localhost:8080/api/nic/update?hostname=$CLAIM_SUBDOMAIN.space.test.rip&myip=192.168.1.100"
if [ "$VERBOSE" = true ]; then
RESPONSE=$(curl -v -u "none:$TOKEN" \
"http://localhost:8080/api/nic/update?hostname=$CLAIM_SUBDOMAIN.space.test.rip&myip=192.168.1.100" 2>&1)
echo "$RESPONSE" | tee /tmp/test5.log
else
RESPONSE=$(curl -s -u "none:$TOKEN" \
"http://localhost:8080/api/nic/update?hostname=$CLAIM_SUBDOMAIN.space.test.rip&myip=192.168.1.100")
fi
log_verbose "Response: $RESPONSE"
# We expect 911 since there's no real DNS server, but auth should work
if [ $? -eq 0 ]; then
echo -e "${GREEN}PASS${NC} (endpoint reachable, response: $(echo "$RESPONSE" | head -c 50))"
else
echo -e "${RED}FAIL${NC}"
exit 1
fi
# Test 6: Profanity filter
echo -n "Test 6: Profanity filter... "
log_verbose "Running: curl -v -X POST http://localhost:8080/api/claim with profane subdomain"
if [ "$VERBOSE" = true ]; then
RESPONSE=$(curl -v -X POST http://localhost:8080/api/claim \
-H "Content-Type: application/json" \
-d '{"subdomain":"fuck"}' 2>&1)
echo "$RESPONSE" | tee /tmp/test6.log
else
RESPONSE=$(curl -s -X POST http://localhost:8080/api/claim \
-H "Content-Type: application/json" \
-d '{"subdomain":"fuck"}')
fi
log_verbose "Response: $RESPONSE"
if echo "$RESPONSE" | grep -q 'inappropriate'; then
echo -e "${GREEN}PASS${NC}"
else
echo -e "${RED}FAIL${NC} - Response: $RESPONSE"
exit 1
fi
# Test 7: Custom filter (DWS terms)
echo -n "Test 7: Custom filter (DWS terms)... "
log_verbose "Running: curl -v -X POST http://localhost:8080/api/claim with reserved subdomain"
if [ "$VERBOSE" = true ]; then
RESPONSE=$(curl -v -X POST http://localhost:8080/api/claim \
-H "Content-Type: application/json" \
-d '{"subdomain":"dws"}' 2>&1)
echo "$RESPONSE" | tee /tmp/test7.log
else
RESPONSE=$(curl -s -X POST http://localhost:8080/api/claim \
-H "Content-Type: application/json" \
-d '{"subdomain":"dws"}')
fi
log_verbose "Response: $RESPONSE"
if echo "$RESPONSE" | grep -q 'reserved'; then
echo -e "${GREEN}PASS${NC}"
else
echo -e "${RED}FAIL${NC} - Response: $RESPONSE"
exit 1
fi
# Test 8: Invalid subdomain format
echo -n "Test 8: Invalid subdomain format... "
log_verbose "Running: curl -v -X POST http://localhost:8080/api/claim with invalid subdomain"
if [ "$VERBOSE" = true ]; then
RESPONSE=$(curl -v -X POST http://localhost:8080/api/claim \
-H "Content-Type: application/json" \
-d '{"subdomain":"ab"}' 2>&1)
echo "$RESPONSE" | tee /tmp/test8.log
else
RESPONSE=$(curl -s -X POST http://localhost:8080/api/claim \
-H "Content-Type: application/json" \
-d '{"subdomain":"ab"}')
fi
log_verbose "Response: $RESPONSE"
if echo "$RESPONSE" | grep -q 'Invalid'; then
echo -e "${GREEN}PASS${NC}"
else
echo -e "${RED}FAIL${NC} - Response: $RESPONSE"
exit 1
fi
echo -e "${GREEN}=== All tests passed! ===${NC}"
if [ "$VERBOSE" = true ]; then
echo -e "${BLUE}=== Detailed logs saved to: ===${NC}"
ls -la /tmp/test*.log 2>/dev/null || echo "No detailed logs found"
fi

View File

@@ -0,0 +1,439 @@
package integration
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
"git.dws.rip/DWS/dyn/internal/config"
"git.dws.rip/DWS/dyn/internal/database"
"git.dws.rip/DWS/dyn/internal/dns"
"git.dws.rip/DWS/dyn/internal/handlers"
"git.dws.rip/DWS/dyn/internal/models"
"git.dws.rip/DWS/dyn/internal/testutil"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func setupIntegrationTest(t *testing.T) (*gin.Engine, *database.DB, *testutil.MockTechnitiumServer, func()) {
// Set Gin to test mode
gin.SetMode(gin.TestMode)
// Create temp database
tmpDB := "/tmp/test_integration_" + time.Now().Format("20060102150405") + ".db"
db, err := database.New(tmpDB)
require.NoError(t, err)
// Create mock Technitium server
mockDNS := testutil.NewMockTechnitiumServer()
// Create config
cfg := &config.Config{
BaseDomain: "test.rip",
SpaceSubdomain: "space",
ServerPort: "8080",
RateLimitPerIP: 100,
RateLimitPerToken: 100,
}
// Create DNS client pointing to mock
dnsClient := dns.NewClient(
mockDNS.URL(),
mockDNS.Token,
mockDNS.Username,
mockDNS.Password,
)
// Setup Gin router
router := gin.New()
router.LoadHTMLGlob("../../web/templates/*")
webHandler := handlers.NewWebHandler(db, cfg)
dynHandler := handlers.NewDynDNSHandler(db, dnsClient, cfg)
router.GET("/", webHandler.Index)
api := router.Group("/api")
{
api.GET("/check", webHandler.CheckSubdomain)
api.POST("/claim", webHandler.ClaimSpace)
api.GET("/nic/update", dynHandler.Update)
}
cleanup := func() {
db.Close()
mockDNS.Close()
os.Remove(tmpDB)
}
return router, db, mockDNS, cleanup
}
func TestIntegration_ClaimSpace(t *testing.T) {
router, _, _, cleanup := setupIntegrationTest(t)
defer cleanup()
tests := []struct {
name string
subdomain string
wantStatus int
wantError bool
}{
{
name: "successful claim",
subdomain: "myhome",
wantStatus: http.StatusCreated,
wantError: false,
},
{
name: "duplicate claim",
subdomain: "myhome",
wantStatus: http.StatusConflict,
wantError: true,
},
{
name: "profane subdomain blocked",
subdomain: "fuck",
wantStatus: http.StatusBadRequest,
wantError: true,
},
{
name: "reserved subdomain blocked",
subdomain: "dws",
wantStatus: http.StatusBadRequest,
wantError: true,
},
{
name: "invalid format",
subdomain: "ab",
wantStatus: http.StatusBadRequest,
wantError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
body, _ := json.Marshal(models.CreateSpaceRequest{
Subdomain: tt.subdomain,
})
req := httptest.NewRequest(http.MethodPost, "/api/claim", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, tt.wantStatus, w.Code)
if !tt.wantError {
var resp models.CreateSpaceResponse
err := json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
assert.NotEmpty(t, resp.Token)
assert.Equal(t, tt.subdomain, resp.Subdomain)
assert.Equal(t, tt.subdomain+".space.test.rip", resp.FQDN)
}
})
}
}
func TestIntegration_CheckSubdomain(t *testing.T) {
router, db, _, cleanup := setupIntegrationTest(t)
defer cleanup()
// Create a space first
ctx := context.Background()
_, err := db.CreateSpace(ctx, "existing")
require.NoError(t, err)
tests := []struct {
name string
subdomain string
wantAvailable bool
wantReason string
}{
{
name: "available subdomain",
subdomain: "newspace",
wantAvailable: true,
wantReason: "",
},
{
name: "existing subdomain",
subdomain: "existing",
wantAvailable: false,
wantReason: "",
},
{
name: "profane subdomain",
subdomain: "shit",
wantAvailable: false,
wantReason: "inappropriate",
},
{
name: "reserved subdomain",
subdomain: "dubey",
wantAvailable: false,
wantReason: "reserved",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/api/check?subdomain="+tt.subdomain, nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
assert.Equal(t, tt.wantAvailable, resp["available"])
if tt.wantReason != "" {
assert.Equal(t, tt.wantReason, resp["reason"])
}
})
}
}
func TestIntegration_DynDNSUpdate(t *testing.T) {
router, db, mockDNS, cleanup := setupIntegrationTest(t)
defer cleanup()
// Create a space
ctx := context.Background()
space, err := db.CreateSpace(ctx, "myrouter")
require.NoError(t, err)
// Create basic auth header
auth := base64.StdEncoding.EncodeToString([]byte("none:" + space.Token))
tests := []struct {
name string
hostname string
myip string
authHeader string
wantStatus int
wantBody string
}{
{
name: "successful update with IP",
hostname: "myrouter.space.test.rip",
myip: "192.168.1.100",
authHeader: "Basic " + auth,
wantStatus: http.StatusOK,
wantBody: "good 192.168.1.100",
},
{
name: "same IP - no change",
hostname: "myrouter.space.test.rip",
myip: "192.168.1.100",
authHeader: "Basic " + auth,
wantStatus: http.StatusOK,
wantBody: "nochg 192.168.1.100",
},
{
name: "different IP",
hostname: "myrouter.space.test.rip",
myip: "10.0.0.50",
authHeader: "Basic " + auth,
wantStatus: http.StatusOK,
wantBody: "good 10.0.0.50",
},
{
name: "missing auth",
hostname: "myrouter.space.test.rip",
myip: "192.168.1.1",
authHeader: "",
wantStatus: http.StatusUnauthorized,
wantBody: "badauth",
},
{
name: "invalid token",
hostname: "myrouter.space.test.rip",
myip: "192.168.1.1",
authHeader: "Basic " + base64.StdEncoding.EncodeToString([]byte("none:invalid-token")),
wantStatus: http.StatusUnauthorized,
wantBody: "badauth",
},
{
name: "wrong hostname",
hostname: "wrong.space.test.rip",
myip: "192.168.1.1",
authHeader: "Basic " + auth,
wantStatus: http.StatusBadRequest,
wantBody: "nohost",
},
{
name: "missing hostname",
hostname: "",
myip: "192.168.1.1",
authHeader: "Basic " + auth,
wantStatus: http.StatusBadRequest,
wantBody: "nohost",
},
{
name: "invalid IP",
hostname: "myrouter.space.test.rip",
myip: "not-an-ip",
authHeader: "Basic " + auth,
wantStatus: http.StatusBadRequest,
wantBody: "dnserr",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
url := "/api/nic/update?hostname=" + tt.hostname
if tt.myip != "" {
url += "&myip=" + tt.myip
}
req := httptest.NewRequest(http.MethodGet, url, nil)
if tt.authHeader != "" {
req.Header.Set("Authorization", tt.authHeader)
}
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, tt.wantStatus, w.Code)
assert.Contains(t, w.Body.String(), tt.wantBody)
})
}
// Verify DNS records were created
records := mockDNS.GetRecords()
assert.Len(t, records, 2) // A record + wildcard record
}
func TestIntegration_FullWorkflow(t *testing.T) {
router, _, mockDNS, cleanup := setupIntegrationTest(t)
defer cleanup()
// Step 1: Check if subdomain is available
req := httptest.NewRequest(http.MethodGet, "/api/check?subdomain=myhome", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var checkResp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &checkResp)
assert.True(t, checkResp["available"].(bool))
// Step 2: Claim the space
claimBody, _ := json.Marshal(models.CreateSpaceRequest{Subdomain: "myhome"})
req = httptest.NewRequest(http.MethodPost, "/api/claim", bytes.NewBuffer(claimBody))
req.Header.Set("Content-Type", "application/json")
w = httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code)
var claimResp models.CreateSpaceResponse
json.Unmarshal(w.Body.Bytes(), &claimResp)
token := claimResp.Token
require.NotEmpty(t, token)
// Step 3: Update DNS via DynDNS protocol
auth := base64.StdEncoding.EncodeToString([]byte("none:" + token))
req = httptest.NewRequest(http.MethodGet, "/api/nic/update?hostname=myhome.space.test.rip&myip=1.2.3.4", nil)
req.Header.Set("Authorization", "Basic "+auth)
w = httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "good 1.2.3.4")
// Step 4: Verify DNS records were created in mock server
records := mockDNS.GetRecords()
assert.Len(t, records, 2)
// Check for A record
aRecord, exists := records["myhome.space.test.rip:A"]
assert.True(t, exists)
assert.Equal(t, "1.2.3.4", aRecord.IPAddress)
// Check for wildcard record
wildcardRecord, exists := records["*.myhome.space.test.rip:A"]
assert.True(t, exists)
assert.Equal(t, "1.2.3.4", wildcardRecord.IPAddress)
// Step 5: Try to claim same subdomain (should fail)
req = httptest.NewRequest(http.MethodPost, "/api/claim", bytes.NewBuffer(claimBody))
req.Header.Set("Content-Type", "application/json")
w = httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusConflict, w.Code)
}
func TestIntegration_ProfanityFiltering(t *testing.T) {
router, _, _, cleanup := setupIntegrationTest(t)
defer cleanup()
profaneSubdomains := []string{
"fuck",
"shit",
"ass",
"bitch",
}
for _, subdomain := range profaneSubdomains {
t.Run(subdomain, func(t *testing.T) {
body, _ := json.Marshal(models.CreateSpaceRequest{Subdomain: subdomain})
req := httptest.NewRequest(http.MethodPost, "/api/claim", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
var resp map[string]string
json.Unmarshal(w.Body.Bytes(), &resp)
assert.Contains(t, resp["error"], "inappropriate")
})
}
}
func TestIntegration_CustomFilter(t *testing.T) {
router, _, _, cleanup := setupIntegrationTest(t)
defer cleanup()
reservedSubdomains := []string{
"dws",
"dubey",
"tanishq",
"tdubey",
"dub3y",
"t4nishq",
"dwsengineering",
"dubeydns",
}
for _, subdomain := range reservedSubdomains {
t.Run(subdomain, func(t *testing.T) {
body, _ := json.Marshal(models.CreateSpaceRequest{Subdomain: subdomain})
req := httptest.NewRequest(http.MethodPost, "/api/claim", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
var resp map[string]string
json.Unmarshal(w.Body.Bytes(), &resp)
assert.Contains(t, resp["error"], "reserved")
})
}
}