Add comprehensive test suite
This commit is contained in:
439
tests/integration/integration_test.go
Normal file
439
tests/integration/integration_test.go
Normal 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")
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user