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") }) } }