Implement backend model for test results and schedule
This commit is contained in:
parent
dbe5339eeb
commit
c8b9d553e1
16 changed files with 1792 additions and 25 deletions
580
internal/api/controller/testresult_controller.go
Normal file
580
internal/api/controller/testresult_controller.go
Normal file
|
|
@ -0,0 +1,580 @@
|
||||||
|
// This file is part of the happyDomain (R) project.
|
||||||
|
// Copyright (c) 2020-2026 happyDomain
|
||||||
|
// Authors: Pierre-Olivier Mercier, et al.
|
||||||
|
//
|
||||||
|
// This program is offered under a commercial and under the AGPL license.
|
||||||
|
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||||
|
//
|
||||||
|
// For AGPL licensing:
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"maps"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"git.happydns.org/happyDomain/internal/api/middleware"
|
||||||
|
"git.happydns.org/happyDomain/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestResultController handles test result operations
|
||||||
|
type TestResultController struct {
|
||||||
|
scope happydns.TestScopeType
|
||||||
|
testPluginUC happydns.TestPluginUsecase
|
||||||
|
testResultUC happydns.TestResultUsecase
|
||||||
|
testScheduleUC happydns.TestScheduleUsecase
|
||||||
|
testScheduler TestSchedulerInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSchedulerInterface defines the interface for triggering on-demand tests
|
||||||
|
type TestSchedulerInterface interface {
|
||||||
|
TriggerOnDemandTest(pluginName string, targetType happydns.TestScopeType, targetID happydns.Identifier, userID happydns.Identifier, options happydns.PluginOptions) (happydns.Identifier, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTestResultController(
|
||||||
|
scope happydns.TestScopeType,
|
||||||
|
testPluginUC happydns.TestPluginUsecase,
|
||||||
|
testResultUC happydns.TestResultUsecase,
|
||||||
|
testScheduleUC happydns.TestScheduleUsecase,
|
||||||
|
testScheduler TestSchedulerInterface,
|
||||||
|
) *TestResultController {
|
||||||
|
return &TestResultController{
|
||||||
|
scope: scope,
|
||||||
|
testPluginUC: testPluginUC,
|
||||||
|
testResultUC: testResultUC,
|
||||||
|
testScheduleUC: testScheduleUC,
|
||||||
|
testScheduler: testScheduler,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTargetFromContext extracts the target ID from context based on scope
|
||||||
|
func (tc *TestResultController) getTargetFromContext(c *gin.Context) (happydns.Identifier, error) {
|
||||||
|
switch tc.scope {
|
||||||
|
case happydns.TestScopeUser:
|
||||||
|
user := c.MustGet("user").(*happydns.User)
|
||||||
|
return user.Id, nil
|
||||||
|
case happydns.TestScopeDomain:
|
||||||
|
domain := c.MustGet("domain").(*happydns.Domain)
|
||||||
|
return domain.Id, nil
|
||||||
|
case happydns.TestScopeService:
|
||||||
|
// Services are stored by ID in context
|
||||||
|
serviceID := c.MustGet("serviceid").(happydns.Identifier)
|
||||||
|
return serviceID, nil
|
||||||
|
default:
|
||||||
|
return happydns.Identifier{}, fmt.Errorf("unsupported scope")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAvailableTests lists all available test plugins for the target scope
|
||||||
|
//
|
||||||
|
// @Summary List available tests
|
||||||
|
// @Description Retrieves all available test plugins for the target scope with their last execution status if enabled
|
||||||
|
// @Tags tests
|
||||||
|
// @Produce json
|
||||||
|
// @Param domain path string true "Domain identifier"
|
||||||
|
// @Success 200 {array} object "List of available tests"
|
||||||
|
// @Failure 500 {object} happydns.ErrorResponse
|
||||||
|
// @Router /domains/{domain}/tests [get]
|
||||||
|
func (tc *TestResultController) ListAvailableTests(c *gin.Context) {
|
||||||
|
targetID, err := tc.getTargetFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all test plugins
|
||||||
|
plugins, err := tc.testPluginUC.ListTestPlugins()
|
||||||
|
if err != nil {
|
||||||
|
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get schedules for this target
|
||||||
|
schedules, err := tc.testScheduleUC.ListSchedulesByTarget(tc.scope, targetID)
|
||||||
|
if err != nil {
|
||||||
|
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build schedule map
|
||||||
|
scheduleMap := make(map[string]*happydns.TestSchedule)
|
||||||
|
for _, sched := range schedules {
|
||||||
|
scheduleMap[sched.PluginName] = sched
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build response with last results
|
||||||
|
type TestInfo struct {
|
||||||
|
PluginName string `json:"plugin_name"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Schedule *happydns.TestSchedule `json:"schedule,omitempty"`
|
||||||
|
LastResult *happydns.TestResult `json:"last_result,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var tests []TestInfo
|
||||||
|
for _, plugin := range plugins {
|
||||||
|
// Get plugin version info
|
||||||
|
versionInfo := plugin.Version()
|
||||||
|
availability := versionInfo.AvailableOn
|
||||||
|
|
||||||
|
// Filter plugins by scope
|
||||||
|
if tc.scope == happydns.TestScopeDomain && !availability.ApplyToDomain {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if tc.scope == happydns.TestScopeService && !availability.ApplyToService {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginNames := plugin.PluginEnvName()
|
||||||
|
if len(pluginNames) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
info := TestInfo{
|
||||||
|
PluginName: pluginNames[0],
|
||||||
|
Enabled: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there's a schedule
|
||||||
|
if sched, ok := scheduleMap[versionInfo.Name]; ok {
|
||||||
|
info.Enabled = sched.Enabled
|
||||||
|
info.Schedule = sched
|
||||||
|
|
||||||
|
// Get last result
|
||||||
|
results, err := tc.testResultUC.ListTestResultsByTarget(versionInfo.Name, tc.scope, targetID, 1)
|
||||||
|
if err == nil && len(results) > 0 {
|
||||||
|
info.LastResult = results[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tests = append(tests, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, tests)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListLatestTestResults retrieves the latest test results for a specific plugin
|
||||||
|
//
|
||||||
|
// @Summary Get latest test results
|
||||||
|
// @Description Retrieves the 5 most recent test results for a specific plugin and target
|
||||||
|
// @Tags tests
|
||||||
|
// @Produce json
|
||||||
|
// @Param domain path string true "Domain identifier"
|
||||||
|
// @Param tname path string true "Test plugin name"
|
||||||
|
// @Success 200 {array} happydns.TestResult
|
||||||
|
// @Failure 500 {object} happydns.ErrorResponse
|
||||||
|
// @Router /domains/{domain}/tests/{tname} [get]
|
||||||
|
func (tc *TestResultController) ListLatestTestResults(c *gin.Context) {
|
||||||
|
pluginName := c.Param("tname")
|
||||||
|
targetID, err := tc.getTargetFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := tc.testResultUC.ListTestResultsByTarget(pluginName, tc.scope, targetID, 5)
|
||||||
|
if err != nil {
|
||||||
|
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, results)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TriggerTest triggers an on-demand test execution
|
||||||
|
//
|
||||||
|
// @Summary Trigger test execution
|
||||||
|
// @Description Triggers an immediate test execution and returns the execution ID
|
||||||
|
// @Tags tests
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param domain path string true "Domain identifier"
|
||||||
|
// @Param tname path string true "Test plugin name"
|
||||||
|
// @Param body body object false "Optional: Plugin options"
|
||||||
|
// @Success 202 {object} object{execution_id=string}
|
||||||
|
// @Failure 400 {object} happydns.ErrorResponse
|
||||||
|
// @Failure 500 {object} happydns.ErrorResponse
|
||||||
|
// @Router /domains/{domain}/tests/{tname} [post]
|
||||||
|
func (tc *TestResultController) TriggerTest(c *gin.Context) {
|
||||||
|
user := middleware.MyUser(c)
|
||||||
|
pluginName := c.Param("tname")
|
||||||
|
targetID, err := tc.getTargetFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse run options
|
||||||
|
var options happydns.SetPluginOptionsRequest
|
||||||
|
if err = c.ShouldBindJSON(&options); err != nil {
|
||||||
|
middleware.ErrorResponse(c, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge options with upper levels (user, domain, service)
|
||||||
|
var domainID, serviceID *happydns.Identifier
|
||||||
|
switch tc.scope {
|
||||||
|
case happydns.TestScopeDomain:
|
||||||
|
domainID = &targetID
|
||||||
|
case happydns.TestScopeService:
|
||||||
|
serviceID = &targetID
|
||||||
|
}
|
||||||
|
|
||||||
|
mergedOptions := make(happydns.PluginOptions)
|
||||||
|
|
||||||
|
// Fill opts with default plugin options
|
||||||
|
plugin, err := tc.testPluginUC.GetTestPlugin(pluginName)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Warning: unable to get plugin %q for default options: %v", pluginName, err)
|
||||||
|
} else {
|
||||||
|
availableOpts := plugin.AvailableOptions()
|
||||||
|
|
||||||
|
// Collect all option documentation from different scopes
|
||||||
|
allOpts := []happydns.PluginOptionDocumentation{}
|
||||||
|
allOpts = append(allOpts, availableOpts.RunOpts...)
|
||||||
|
allOpts = append(allOpts, availableOpts.ServiceOpts...)
|
||||||
|
allOpts = append(allOpts, availableOpts.DomainOpts...)
|
||||||
|
allOpts = append(allOpts, availableOpts.UserOpts...)
|
||||||
|
allOpts = append(allOpts, availableOpts.AdminOpts...)
|
||||||
|
|
||||||
|
// Fill defaults
|
||||||
|
for _, opt := range allOpts {
|
||||||
|
if opt.Default != nil {
|
||||||
|
mergedOptions[opt.Id] = opt.Default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get merged options from upper levels
|
||||||
|
baseOptions, err := tc.testPluginUC.GetTestPluginOptions(pluginName, &user.Id, domainID, serviceID)
|
||||||
|
if err != nil {
|
||||||
|
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge request options on top of base options (request options override)
|
||||||
|
if baseOptions != nil {
|
||||||
|
maps.Copy(mergedOptions, *baseOptions)
|
||||||
|
}
|
||||||
|
maps.Copy(mergedOptions, options.Options)
|
||||||
|
|
||||||
|
// Trigger the test via scheduler (returns error if scheduler is disabled)
|
||||||
|
executionID, err := tc.testScheduler.TriggerOnDemandTest(pluginName, tc.scope, targetID, user.Id, mergedOptions)
|
||||||
|
if err != nil {
|
||||||
|
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusAccepted, gin.H{"execution_id": executionID.String()})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTestPluginOptions retrieves plugin options for the target scope
|
||||||
|
//
|
||||||
|
// @Summary Get test plugin options
|
||||||
|
// @Description Retrieves configuration options for a test plugin at the target scope
|
||||||
|
// @Tags tests
|
||||||
|
// @Produce json
|
||||||
|
// @Param domain path string true "Domain identifier"
|
||||||
|
// @Param tname path string true "Test plugin name"
|
||||||
|
// @Success 200 {object} happydns.PluginOptions
|
||||||
|
// @Failure 500 {object} happydns.ErrorResponse
|
||||||
|
// @Router /domains/{domain}/tests/{tname}/options [get]
|
||||||
|
func (tc *TestResultController) GetTestPluginOptions(c *gin.Context) {
|
||||||
|
user := middleware.MyUser(c)
|
||||||
|
pluginName := c.Param("tname")
|
||||||
|
targetID, err := tc.getTargetFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var domainID, serviceID *happydns.Identifier
|
||||||
|
switch tc.scope {
|
||||||
|
case happydns.TestScopeDomain:
|
||||||
|
domainID = &targetID
|
||||||
|
case happydns.TestScopeService:
|
||||||
|
serviceID = &targetID
|
||||||
|
}
|
||||||
|
|
||||||
|
opts, err := tc.testPluginUC.GetTestPluginOptions(pluginName, &user.Id, domainID, serviceID)
|
||||||
|
if err != nil {
|
||||||
|
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddTestPluginOptions adds or overwrites specific options
|
||||||
|
//
|
||||||
|
// @Summary Add test plugin options
|
||||||
|
// @Description Adds or overwrites specific options for a test plugin at the target scope
|
||||||
|
// @Tags tests
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param domain path string true "Domain identifier"
|
||||||
|
// @Param tname path string true "Test plugin name"
|
||||||
|
// @Param body body happydns.PluginOptions true "Options to add"
|
||||||
|
// @Success 200 {object} bool
|
||||||
|
// @Failure 400 {object} happydns.ErrorResponse
|
||||||
|
// @Failure 500 {object} happydns.ErrorResponse
|
||||||
|
// @Router /domains/{domain}/tests/{tname}/options [post]
|
||||||
|
func (tc *TestResultController) AddTestPluginOptions(c *gin.Context) {
|
||||||
|
user := middleware.MyUser(c)
|
||||||
|
pluginName := c.Param("tname")
|
||||||
|
targetID, err := tc.getTargetFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var options happydns.PluginOptions
|
||||||
|
if err = c.ShouldBindJSON(&options); err != nil {
|
||||||
|
middleware.ErrorResponse(c, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var domainID, serviceID *happydns.Identifier
|
||||||
|
switch tc.scope {
|
||||||
|
case happydns.TestScopeDomain:
|
||||||
|
domainID = &targetID
|
||||||
|
case happydns.TestScopeService:
|
||||||
|
serviceID = &targetID
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tc.testPluginUC.OverwriteSomeTestPluginOptions(pluginName, &user.Id, domainID, serviceID, options)
|
||||||
|
if err != nil {
|
||||||
|
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeTestPluginOptions replaces all options
|
||||||
|
//
|
||||||
|
// @Summary Replace test plugin options
|
||||||
|
// @Description Replaces all options for a test plugin at the target scope
|
||||||
|
// @Tags tests
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param domain path string true "Domain identifier"
|
||||||
|
// @Param tname path string true "Test plugin name"
|
||||||
|
// @Param body body happydns.PluginOptions true "New complete options"
|
||||||
|
// @Success 200 {object} bool
|
||||||
|
// @Failure 400 {object} happydns.ErrorResponse
|
||||||
|
// @Failure 500 {object} happydns.ErrorResponse
|
||||||
|
// @Router /domains/{domain}/tests/{tname}/options [put]
|
||||||
|
func (tc *TestResultController) ChangeTestPluginOptions(c *gin.Context) {
|
||||||
|
user := middleware.MyUser(c)
|
||||||
|
pluginName := c.Param("tname")
|
||||||
|
targetID, err := tc.getTargetFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var options happydns.PluginOptions
|
||||||
|
if err = c.ShouldBindJSON(&options); err != nil {
|
||||||
|
middleware.ErrorResponse(c, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var domainID, serviceID *happydns.Identifier
|
||||||
|
switch tc.scope {
|
||||||
|
case happydns.TestScopeDomain:
|
||||||
|
domainID = &targetID
|
||||||
|
case happydns.TestScopeService:
|
||||||
|
serviceID = &targetID
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tc.testPluginUC.SetTestPluginOptions(pluginName, &user.Id, domainID, serviceID, options)
|
||||||
|
if err != nil {
|
||||||
|
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTestExecutionStatus retrieves the status of a test execution
|
||||||
|
//
|
||||||
|
// @Summary Get test execution status
|
||||||
|
// @Description Retrieves the current status of a test execution
|
||||||
|
// @Tags tests
|
||||||
|
// @Produce json
|
||||||
|
// @Param domain path string true "Domain identifier"
|
||||||
|
// @Param tname path string true "Test plugin name"
|
||||||
|
// @Param execution_id path string true "Execution ID"
|
||||||
|
// @Success 200 {object} happydns.TestExecution
|
||||||
|
// @Failure 404 {object} happydns.ErrorResponse
|
||||||
|
// @Failure 500 {object} happydns.ErrorResponse
|
||||||
|
// @Router /domains/{domain}/tests/{tname}/executions/{execution_id} [get]
|
||||||
|
func (tc *TestResultController) GetTestExecutionStatus(c *gin.Context) {
|
||||||
|
executionIDStr := c.Param("execution_id")
|
||||||
|
executionID, err := happydns.NewIdentifierFromString(executionIDStr)
|
||||||
|
if err != nil {
|
||||||
|
middleware.ErrorResponse(c, http.StatusBadRequest, fmt.Errorf("invalid execution ID"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
execution, err := tc.testResultUC.GetTestExecution(executionID)
|
||||||
|
if err != nil {
|
||||||
|
middleware.ErrorResponse(c, http.StatusNotFound, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, execution)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListTestPluginResults lists all results for a test plugin
|
||||||
|
//
|
||||||
|
// @Summary List test results
|
||||||
|
// @Description Lists all test results for a specific test plugin and target
|
||||||
|
// @Tags tests
|
||||||
|
// @Produce json
|
||||||
|
// @Param domain path string true "Domain identifier"
|
||||||
|
// @Param tname path string true "Test plugin name"
|
||||||
|
// @Param limit query int false "Maximum number of results to return (default: 10)"
|
||||||
|
// @Success 200 {array} happydns.TestResult
|
||||||
|
// @Failure 500 {object} happydns.ErrorResponse
|
||||||
|
// @Router /domains/{domain}/tests/{tname}/results [get]
|
||||||
|
func (tc *TestResultController) ListTestPluginResults(c *gin.Context) {
|
||||||
|
pluginName := c.Param("tname")
|
||||||
|
targetID, err := tc.getTargetFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse limit parameter
|
||||||
|
limit := 10
|
||||||
|
if limitStr := c.Query("limit"); limitStr != "" {
|
||||||
|
fmt.Sscanf(limitStr, "%d", &limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := tc.testResultUC.ListTestResultsByTarget(pluginName, tc.scope, targetID, limit)
|
||||||
|
if err != nil {
|
||||||
|
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, results)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DropTestPluginResults deletes all results for a test plugin
|
||||||
|
//
|
||||||
|
// @Summary Delete all test results
|
||||||
|
// @Description Deletes all test results for a specific test plugin and target
|
||||||
|
// @Tags tests
|
||||||
|
// @Produce json
|
||||||
|
// @Param domain path string true "Domain identifier"
|
||||||
|
// @Param tname path string true "Test plugin name"
|
||||||
|
// @Success 204 "No Content"
|
||||||
|
// @Failure 500 {object} happydns.ErrorResponse
|
||||||
|
// @Router /domains/{domain}/tests/{tname}/results [delete]
|
||||||
|
func (tc *TestResultController) DropTestPluginResults(c *gin.Context) {
|
||||||
|
pluginName := c.Param("tname")
|
||||||
|
targetID, err := tc.getTargetFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tc.testResultUC.DeleteAllTestResults(pluginName, tc.scope, targetID)
|
||||||
|
if err != nil {
|
||||||
|
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTestPluginResult retrieves a specific test result
|
||||||
|
//
|
||||||
|
// @Summary Get test result
|
||||||
|
// @Description Retrieves a specific test result by ID
|
||||||
|
// @Tags tests
|
||||||
|
// @Produce json
|
||||||
|
// @Param domain path string true "Domain identifier"
|
||||||
|
// @Param tname path string true "Test plugin name"
|
||||||
|
// @Param result_id path string true "Result ID"
|
||||||
|
// @Success 200 {object} happydns.TestResult
|
||||||
|
// @Failure 404 {object} happydns.ErrorResponse
|
||||||
|
// @Failure 500 {object} happydns.ErrorResponse
|
||||||
|
// @Router /domains/{domain}/tests/{tname}/results/{result_id} [get]
|
||||||
|
func (tc *TestResultController) GetTestPluginResult(c *gin.Context) {
|
||||||
|
pluginName := c.Param("tname")
|
||||||
|
resultIDStr := c.Param("result_id")
|
||||||
|
targetID, err := tc.getTargetFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resultID, err := happydns.NewIdentifierFromString(resultIDStr)
|
||||||
|
if err != nil {
|
||||||
|
middleware.ErrorResponse(c, http.StatusBadRequest, fmt.Errorf("invalid result ID"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := tc.testResultUC.GetTestResult(pluginName, tc.scope, targetID, resultID)
|
||||||
|
if err != nil {
|
||||||
|
middleware.ErrorResponse(c, http.StatusNotFound, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DropTestPluginResult deletes a specific test result
|
||||||
|
//
|
||||||
|
// @Summary Delete test result
|
||||||
|
// @Description Deletes a specific test result by ID
|
||||||
|
// @Tags tests
|
||||||
|
// @Produce json
|
||||||
|
// @Param domain path string true "Domain identifier"
|
||||||
|
// @Param tname path string true "Test plugin name"
|
||||||
|
// @Param result_id path string true "Result ID"
|
||||||
|
// @Success 204 "No Content"
|
||||||
|
// @Failure 404 {object} happydns.ErrorResponse
|
||||||
|
// @Failure 500 {object} happydns.ErrorResponse
|
||||||
|
// @Router /domains/{domain}/tests/{tname}/results/{result_id} [delete]
|
||||||
|
func (tc *TestResultController) DropTestPluginResult(c *gin.Context) {
|
||||||
|
pluginName := c.Param("tname")
|
||||||
|
resultIDStr := c.Param("result_id")
|
||||||
|
targetID, err := tc.getTargetFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resultID, err := happydns.NewIdentifierFromString(resultIDStr)
|
||||||
|
if err != nil {
|
||||||
|
middleware.ErrorResponse(c, http.StatusBadRequest, fmt.Errorf("invalid result ID"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tc.testResultUC.DeleteTestResult(pluginName, tc.scope, targetID, resultID)
|
||||||
|
if err != nil {
|
||||||
|
middleware.ErrorResponse(c, http.StatusNotFound, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
@ -48,6 +48,10 @@ func DeclareDomainRoutes(router *gin.RouterGroup, dependancies happydns.UsecaseD
|
||||||
|
|
||||||
DeclareDomainLogRoutes(apiDomainsRoutes, dependancies)
|
DeclareDomainLogRoutes(apiDomainsRoutes, dependancies)
|
||||||
|
|
||||||
|
// Declare test result routes for domain scope
|
||||||
|
|
||||||
|
DeclareScopedTestResultRoutes(apiDomainsRoutes, dependancies, happydns.TestScopeDomain)
|
||||||
|
|
||||||
apiDomainsRoutes.POST("/zone", dc.ImportZone)
|
apiDomainsRoutes.POST("/zone", dc.ImportZone)
|
||||||
apiDomainsRoutes.POST("/retrieve_zone", dc.RetrieveZone)
|
apiDomainsRoutes.POST("/retrieve_zone", dc.RetrieveZone)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,4 +40,7 @@ func DeclareZoneServiceRoutes(apiZonesRoutes, apiZonesSubdomainRoutes *gin.Route
|
||||||
apiZonesSubdomainServiceIdRoutes.Use(middleware.ServiceIdHandler(dependancies.ServiceUsecase()))
|
apiZonesSubdomainServiceIdRoutes.Use(middleware.ServiceIdHandler(dependancies.ServiceUsecase()))
|
||||||
apiZonesSubdomainServiceIdRoutes.GET("", sc.GetZoneService)
|
apiZonesSubdomainServiceIdRoutes.GET("", sc.GetZoneService)
|
||||||
apiZonesSubdomainServiceIdRoutes.DELETE("", sc.DeleteZoneService)
|
apiZonesSubdomainServiceIdRoutes.DELETE("", sc.DeleteZoneService)
|
||||||
|
|
||||||
|
// Declare test result routes for service scope
|
||||||
|
DeclareScopedTestResultRoutes(apiZonesSubdomainServiceIdRoutes, dependancies, happydns.TestScopeService)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
98
internal/api/route/testresults.go
Normal file
98
internal/api/route/testresults.go
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
// This file is part of the happyDomain (R) project.
|
||||||
|
// Copyright (c) 2020-2026 happyDomain
|
||||||
|
// Authors: Pierre-Olivier Mercier, et al.
|
||||||
|
//
|
||||||
|
// This program is offered under a commercial and under the AGPL license.
|
||||||
|
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||||
|
//
|
||||||
|
// For AGPL licensing:
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package route
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"git.happydns.org/happyDomain/internal/api/controller"
|
||||||
|
"git.happydns.org/happyDomain/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DeclareScopedTestResultRoutes declares test result routes for a specific scope (domain, zone, or service)
|
||||||
|
func DeclareScopedTestResultRoutes(
|
||||||
|
scopedRouter *gin.RouterGroup,
|
||||||
|
dependancies happydns.UsecaseDependancies,
|
||||||
|
scope happydns.TestScopeType,
|
||||||
|
) {
|
||||||
|
testScheduler := dependancies.TestScheduler()
|
||||||
|
|
||||||
|
tc := controller.NewTestResultController(
|
||||||
|
scope,
|
||||||
|
dependancies.TestPluginUsecase(),
|
||||||
|
dependancies.TestResultUsecase(),
|
||||||
|
dependancies.TestScheduleUsecase(),
|
||||||
|
testScheduler,
|
||||||
|
)
|
||||||
|
|
||||||
|
// List all available tests with their status
|
||||||
|
scopedRouter.GET("/tests", tc.ListAvailableTests)
|
||||||
|
|
||||||
|
// Test-specific routes
|
||||||
|
apiTestsRoutes := scopedRouter.Group("/tests/:tname")
|
||||||
|
{
|
||||||
|
// Get latest results for a test
|
||||||
|
apiTestsRoutes.GET("", tc.ListLatestTestResults)
|
||||||
|
|
||||||
|
// Trigger an on-demand test
|
||||||
|
apiTestsRoutes.POST("", tc.TriggerTest)
|
||||||
|
|
||||||
|
// Manage test plugin options at this scope
|
||||||
|
apiTestsRoutes.GET("/options", tc.GetTestPluginOptions)
|
||||||
|
apiTestsRoutes.POST("/options", tc.AddTestPluginOptions)
|
||||||
|
apiTestsRoutes.PUT("/options", tc.ChangeTestPluginOptions)
|
||||||
|
|
||||||
|
// Test execution routes
|
||||||
|
apiTestExecutionsRoutes := apiTestsRoutes.Group("/executions/:execution_id")
|
||||||
|
{
|
||||||
|
apiTestExecutionsRoutes.GET("", tc.GetTestExecutionStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test results routes
|
||||||
|
apiTestsRoutes.GET("/results", tc.ListTestPluginResults)
|
||||||
|
apiTestsRoutes.DELETE("/results", tc.DropTestPluginResults)
|
||||||
|
|
||||||
|
apiTestResultsRoutes := apiTestsRoutes.Group("/results/:result_id")
|
||||||
|
{
|
||||||
|
apiTestResultsRoutes.GET("", tc.GetTestPluginResult)
|
||||||
|
apiTestResultsRoutes.DELETE("", tc.DropTestPluginResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeclareTestScheduleRoutes declares test schedule management routes
|
||||||
|
func DeclareTestScheduleRoutes(router *gin.RouterGroup, dependancies happydns.UsecaseDependancies) {
|
||||||
|
sc := controller.NewTestScheduleController(dependancies.TestScheduleUsecase())
|
||||||
|
|
||||||
|
schedulesRoutes := router.Group("/plugins/tests/schedules")
|
||||||
|
{
|
||||||
|
schedulesRoutes.GET("", sc.ListTestSchedules)
|
||||||
|
schedulesRoutes.POST("", sc.CreateTestSchedule)
|
||||||
|
|
||||||
|
scheduleRoutes := schedulesRoutes.Group("/:schedule_id")
|
||||||
|
{
|
||||||
|
scheduleRoutes.GET("", sc.GetTestSchedule)
|
||||||
|
scheduleRoutes.PUT("", sc.UpdateTestSchedule)
|
||||||
|
scheduleRoutes.DELETE("", sc.DeleteTestSchedule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -30,6 +30,7 @@ import (
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"git.happydns.org/happyDomain/internal/api/controller"
|
||||||
api "git.happydns.org/happyDomain/internal/api/route"
|
api "git.happydns.org/happyDomain/internal/api/route"
|
||||||
"git.happydns.org/happyDomain/internal/captcha"
|
"git.happydns.org/happyDomain/internal/captcha"
|
||||||
"git.happydns.org/happyDomain/internal/mailer"
|
"git.happydns.org/happyDomain/internal/mailer"
|
||||||
|
|
@ -45,6 +46,7 @@ import (
|
||||||
providerUC "git.happydns.org/happyDomain/internal/usecase/provider"
|
providerUC "git.happydns.org/happyDomain/internal/usecase/provider"
|
||||||
serviceUC "git.happydns.org/happyDomain/internal/usecase/service"
|
serviceUC "git.happydns.org/happyDomain/internal/usecase/service"
|
||||||
sessionUC "git.happydns.org/happyDomain/internal/usecase/session"
|
sessionUC "git.happydns.org/happyDomain/internal/usecase/session"
|
||||||
|
testresultUC "git.happydns.org/happyDomain/internal/usecase/testresult"
|
||||||
userUC "git.happydns.org/happyDomain/internal/usecase/user"
|
userUC "git.happydns.org/happyDomain/internal/usecase/user"
|
||||||
zoneUC "git.happydns.org/happyDomain/internal/usecase/zone"
|
zoneUC "git.happydns.org/happyDomain/internal/usecase/zone"
|
||||||
zoneServiceUC "git.happydns.org/happyDomain/internal/usecase/zone_service"
|
zoneServiceUC "git.happydns.org/happyDomain/internal/usecase/zone_service"
|
||||||
|
|
@ -66,6 +68,7 @@ type Usecases struct {
|
||||||
service happydns.ServiceUsecase
|
service happydns.ServiceUsecase
|
||||||
serviceSpecs happydns.ServiceSpecsUsecase
|
serviceSpecs happydns.ServiceSpecsUsecase
|
||||||
testPlugin happydns.TestPluginUsecase
|
testPlugin happydns.TestPluginUsecase
|
||||||
|
testResult happydns.TestResultUsecase
|
||||||
user happydns.UserUsecase
|
user happydns.UserUsecase
|
||||||
zone happydns.ZoneUsecase
|
zone happydns.ZoneUsecase
|
||||||
zoneService happydns.ZoneServiceUsecase
|
zoneService happydns.ZoneServiceUsecase
|
||||||
|
|
@ -155,6 +158,10 @@ func (a *App) TestPluginUsecase() happydns.TestPluginUsecase {
|
||||||
return a.usecases.testPlugin
|
return a.usecases.testPlugin
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) TestResultUsecase() happydns.TestResultUsecase {
|
||||||
|
return a.usecases.testResult
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) UserUsecase() happydns.UserUsecase {
|
func (a *App) UserUsecase() happydns.UserUsecase {
|
||||||
return a.usecases.user
|
return a.usecases.user
|
||||||
}
|
}
|
||||||
|
|
@ -304,6 +311,7 @@ func (app *App) initUsecases() {
|
||||||
app.usecases.resolver = usecase.NewResolverUsecase(app.cfg)
|
app.usecases.resolver = usecase.NewResolverUsecase(app.cfg)
|
||||||
app.usecases.session = sessionService
|
app.usecases.session = sessionService
|
||||||
app.usecases.testPlugin = pluginUC.NewTestPluginUsecase(app.cfg, app.plugins, app.store)
|
app.usecases.testPlugin = pluginUC.NewTestPluginUsecase(app.cfg, app.plugins, app.store)
|
||||||
|
app.usecases.testResult = testresultUC.NewTestResultUsecase(app.store, app.cfg)
|
||||||
|
|
||||||
app.usecases.orchestrator = orchestrator.NewOrchestrator(
|
app.usecases.orchestrator = orchestrator.NewOrchestrator(
|
||||||
domainLogService,
|
domainLogService,
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,8 @@ func ConsolidateConfig() (opts *happydns.Options, err error) {
|
||||||
MailFrom: mail.Address{Name: "happyDomain", Address: "happydomain@localhost"},
|
MailFrom: mail.Address{Name: "happyDomain", Address: "happydomain@localhost"},
|
||||||
MailSMTPPort: 587,
|
MailSMTPPort: 587,
|
||||||
StorageEngine: "leveldb",
|
StorageEngine: "leveldb",
|
||||||
|
MaxResultsPerTest: 100,
|
||||||
|
ResultRetentionDays: 90,
|
||||||
}
|
}
|
||||||
|
|
||||||
declareFlags(opts)
|
declareFlags(opts)
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import (
|
||||||
"git.happydns.org/happyDomain/internal/usecase/plugin"
|
"git.happydns.org/happyDomain/internal/usecase/plugin"
|
||||||
"git.happydns.org/happyDomain/internal/usecase/provider"
|
"git.happydns.org/happyDomain/internal/usecase/provider"
|
||||||
"git.happydns.org/happyDomain/internal/usecase/session"
|
"git.happydns.org/happyDomain/internal/usecase/session"
|
||||||
|
"git.happydns.org/happyDomain/internal/usecase/testresult"
|
||||||
"git.happydns.org/happyDomain/internal/usecase/user"
|
"git.happydns.org/happyDomain/internal/usecase/user"
|
||||||
"git.happydns.org/happyDomain/internal/usecase/zone"
|
"git.happydns.org/happyDomain/internal/usecase/zone"
|
||||||
"git.happydns.org/happyDomain/model"
|
"git.happydns.org/happyDomain/model"
|
||||||
|
|
@ -47,6 +48,7 @@ type Storage interface {
|
||||||
plugin.PluginStorage
|
plugin.PluginStorage
|
||||||
provider.ProviderStorage
|
provider.ProviderStorage
|
||||||
session.SessionStorage
|
session.SessionStorage
|
||||||
|
testresult.TestResultStorage
|
||||||
user.UserStorage
|
user.UserStorage
|
||||||
zone.ZoneStorage
|
zone.ZoneStorage
|
||||||
|
|
||||||
|
|
|
||||||
433
internal/storage/kvtpl/testresult.go
Normal file
433
internal/storage/kvtpl/testresult.go
Normal file
|
|
@ -0,0 +1,433 @@
|
||||||
|
// This file is part of the happyDomain (R) project.
|
||||||
|
// Copyright (c) 2020-2026 happyDomain
|
||||||
|
// Authors: Pierre-Olivier Mercier, et al.
|
||||||
|
//
|
||||||
|
// This program is offered under a commercial and under the AGPL license.
|
||||||
|
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||||
|
//
|
||||||
|
// For AGPL licensing:
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.happydns.org/happyDomain/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test Result storage keys:
|
||||||
|
// testresult|{plugin-name}|{target-type}|{target-id}|{result-id}
|
||||||
|
func makeTestResultKey(pluginName string, targetType happydns.TestScopeType, targetId, resultId happydns.Identifier) string {
|
||||||
|
return fmt.Sprintf("testresult|%s|%d|%s|%s", pluginName, targetType, targetId.String(), resultId.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeTestResultPrefix(pluginName string, targetType happydns.TestScopeType, targetId happydns.Identifier) string {
|
||||||
|
return fmt.Sprintf("testresult|%s|%d|%s|", pluginName, targetType, targetId.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListTestResults retrieves test results for a specific plugin+target combination
|
||||||
|
func (s *KVStorage) ListTestResults(pluginName string, targetType happydns.TestScopeType, targetId happydns.Identifier, limit int) ([]*happydns.TestResult, error) {
|
||||||
|
prefix := makeTestResultPrefix(pluginName, targetType, targetId)
|
||||||
|
iter := s.db.Search(prefix)
|
||||||
|
defer iter.Release()
|
||||||
|
|
||||||
|
var results []*happydns.TestResult
|
||||||
|
for iter.Next() {
|
||||||
|
var r happydns.TestResult
|
||||||
|
if err := s.db.DecodeData(iter.Value(), &r); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
results = append(results, &r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by ExecutedAt descending (most recent first)
|
||||||
|
sort.Slice(results, func(i, j int) bool {
|
||||||
|
return results[i].ExecutedAt.After(results[j].ExecutedAt)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Apply limit
|
||||||
|
if limit > 0 && len(results) > limit {
|
||||||
|
results = results[:limit]
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListTestResultsByPlugin retrieves all test results for a plugin across all targets for a user
|
||||||
|
func (s *KVStorage) ListTestResultsByPlugin(userId happydns.Identifier, pluginName string, limit int) ([]*happydns.TestResult, error) {
|
||||||
|
prefix := fmt.Sprintf("testresult|%s|", pluginName)
|
||||||
|
iter := s.db.Search(prefix)
|
||||||
|
defer iter.Release()
|
||||||
|
|
||||||
|
var results []*happydns.TestResult
|
||||||
|
for iter.Next() {
|
||||||
|
var r happydns.TestResult
|
||||||
|
if err := s.db.DecodeData(iter.Value(), &r); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Filter by user
|
||||||
|
if r.OwnerId.Equals(userId) {
|
||||||
|
results = append(results, &r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by ExecutedAt descending (most recent first)
|
||||||
|
sort.Slice(results, func(i, j int) bool {
|
||||||
|
return results[i].ExecutedAt.After(results[j].ExecutedAt)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Apply limit
|
||||||
|
if limit > 0 && len(results) > limit {
|
||||||
|
results = results[:limit]
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListTestResultsByUser retrieves all test results for a user
|
||||||
|
func (s *KVStorage) ListTestResultsByUser(userId happydns.Identifier, limit int) ([]*happydns.TestResult, error) {
|
||||||
|
iter := s.db.Search("testresult|")
|
||||||
|
defer iter.Release()
|
||||||
|
|
||||||
|
var results []*happydns.TestResult
|
||||||
|
for iter.Next() {
|
||||||
|
var r happydns.TestResult
|
||||||
|
if err := s.db.DecodeData(iter.Value(), &r); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Filter by user
|
||||||
|
if r.OwnerId.Equals(userId) {
|
||||||
|
results = append(results, &r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by ExecutedAt descending (most recent first)
|
||||||
|
sort.Slice(results, func(i, j int) bool {
|
||||||
|
return results[i].ExecutedAt.After(results[j].ExecutedAt)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Apply limit
|
||||||
|
if limit > 0 && len(results) > limit {
|
||||||
|
results = results[:limit]
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTestResult retrieves a specific test result by its ID
|
||||||
|
func (s *KVStorage) GetTestResult(pluginName string, targetType happydns.TestScopeType, targetId happydns.Identifier, resultId happydns.Identifier) (*happydns.TestResult, error) {
|
||||||
|
key := makeTestResultKey(pluginName, targetType, targetId, resultId)
|
||||||
|
var result happydns.TestResult
|
||||||
|
err := s.db.Get(key, &result)
|
||||||
|
if errors.Is(err, happydns.ErrNotFound) {
|
||||||
|
return nil, happydns.ErrTestResultNotFound
|
||||||
|
}
|
||||||
|
return &result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTestResult stores a new test result
|
||||||
|
func (s *KVStorage) CreateTestResult(result *happydns.TestResult) error {
|
||||||
|
prefix := makeTestResultPrefix(result.PluginName, result.TestType, result.TargetId)
|
||||||
|
key, id, err := s.db.FindIdentifierKey(prefix)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Id = id
|
||||||
|
return s.db.Put(key, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteTestResult removes a specific test result
|
||||||
|
func (s *KVStorage) DeleteTestResult(pluginName string, targetType happydns.TestScopeType, targetId happydns.Identifier, resultId happydns.Identifier) error {
|
||||||
|
key := makeTestResultKey(pluginName, targetType, targetId, resultId)
|
||||||
|
return s.db.Delete(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteOldTestResults removes old test results keeping only the most recent N results
|
||||||
|
func (s *KVStorage) DeleteOldTestResults(pluginName string, targetType happydns.TestScopeType, targetId happydns.Identifier, keepCount int) error {
|
||||||
|
results, err := s.ListTestResults(pluginName, targetType, targetId, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Results are already sorted by ExecutedAt descending
|
||||||
|
// Delete results beyond keepCount
|
||||||
|
if len(results) > keepCount {
|
||||||
|
for _, r := range results[keepCount:] {
|
||||||
|
if err := s.DeleteTestResult(pluginName, targetType, targetId, r.Id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Schedule storage keys:
|
||||||
|
// testschedule|{schedule-id}
|
||||||
|
// testschedule.byuser|{user-id}|{schedule-id}
|
||||||
|
// testschedule.bytarget|{target-type}|{target-id}|{schedule-id}
|
||||||
|
|
||||||
|
func makeTestScheduleKey(scheduleId happydns.Identifier) string {
|
||||||
|
return fmt.Sprintf("testschedule|%s", scheduleId.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeTestScheduleUserIndexKey(userId, scheduleId happydns.Identifier) string {
|
||||||
|
return fmt.Sprintf("testschedule.byuser|%s|%s", userId.String(), scheduleId.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeTestScheduleTargetIndexKey(targetType happydns.TestScopeType, targetId, scheduleId happydns.Identifier) string {
|
||||||
|
return fmt.Sprintf("testschedule.bytarget|%d|%s|%s", targetType, targetId.String(), scheduleId.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListEnabledTestSchedules retrieves all enabled schedules
|
||||||
|
func (s *KVStorage) ListEnabledTestSchedules() ([]*happydns.TestSchedule, error) {
|
||||||
|
iter := s.db.Search("testschedule|")
|
||||||
|
defer iter.Release()
|
||||||
|
|
||||||
|
var schedules []*happydns.TestSchedule
|
||||||
|
for iter.Next() {
|
||||||
|
var sched happydns.TestSchedule
|
||||||
|
if err := s.db.DecodeData(iter.Value(), &sched); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if sched.Enabled {
|
||||||
|
schedules = append(schedules, &sched)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return schedules, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListTestSchedulesByUser retrieves all schedules for a specific user
|
||||||
|
func (s *KVStorage) ListTestSchedulesByUser(userId happydns.Identifier) ([]*happydns.TestSchedule, error) {
|
||||||
|
prefix := fmt.Sprintf("testschedule.byuser|%s|", userId.String())
|
||||||
|
iter := s.db.Search(prefix)
|
||||||
|
defer iter.Release()
|
||||||
|
|
||||||
|
var schedules []*happydns.TestSchedule
|
||||||
|
for iter.Next() {
|
||||||
|
// Extract schedule ID from index key
|
||||||
|
key := string(iter.Key())
|
||||||
|
parts := strings.Split(key, "|")
|
||||||
|
if len(parts) < 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleId, err := happydns.NewIdentifierFromString(parts[2])
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the actual schedule
|
||||||
|
var sched happydns.TestSchedule
|
||||||
|
schedKey := makeTestScheduleKey(scheduleId)
|
||||||
|
if err := s.db.Get(schedKey, &sched); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
schedules = append(schedules, &sched)
|
||||||
|
}
|
||||||
|
|
||||||
|
return schedules, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListTestSchedulesByTarget retrieves all schedules for a specific target
|
||||||
|
func (s *KVStorage) ListTestSchedulesByTarget(targetType happydns.TestScopeType, targetId happydns.Identifier) ([]*happydns.TestSchedule, error) {
|
||||||
|
prefix := fmt.Sprintf("testschedule.bytarget|%d|%s|", targetType, targetId.String())
|
||||||
|
iter := s.db.Search(prefix)
|
||||||
|
defer iter.Release()
|
||||||
|
|
||||||
|
var schedules []*happydns.TestSchedule
|
||||||
|
for iter.Next() {
|
||||||
|
// Extract schedule ID from index key
|
||||||
|
key := string(iter.Key())
|
||||||
|
parts := strings.Split(key, "|")
|
||||||
|
if len(parts) < 4 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleId, err := happydns.NewIdentifierFromString(parts[3])
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the actual schedule
|
||||||
|
var sched happydns.TestSchedule
|
||||||
|
schedKey := makeTestScheduleKey(scheduleId)
|
||||||
|
if err := s.db.Get(schedKey, &sched); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
schedules = append(schedules, &sched)
|
||||||
|
}
|
||||||
|
|
||||||
|
return schedules, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTestSchedule retrieves a specific schedule by ID
|
||||||
|
func (s *KVStorage) GetTestSchedule(scheduleId happydns.Identifier) (*happydns.TestSchedule, error) {
|
||||||
|
key := makeTestScheduleKey(scheduleId)
|
||||||
|
var schedule happydns.TestSchedule
|
||||||
|
err := s.db.Get(key, &schedule)
|
||||||
|
if errors.Is(err, happydns.ErrNotFound) {
|
||||||
|
return nil, happydns.ErrTestScheduleNotFound
|
||||||
|
}
|
||||||
|
return &schedule, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTestSchedule creates a new test schedule
|
||||||
|
func (s *KVStorage) CreateTestSchedule(schedule *happydns.TestSchedule) error {
|
||||||
|
key, id, err := s.db.FindIdentifierKey("testschedule|")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
schedule.Id = id
|
||||||
|
|
||||||
|
// Store the schedule
|
||||||
|
if err := s.db.Put(key, schedule); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create indexes
|
||||||
|
userIndexKey := makeTestScheduleUserIndexKey(schedule.OwnerId, schedule.Id)
|
||||||
|
if err := s.db.Put(userIndexKey, []byte{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
targetIndexKey := makeTestScheduleTargetIndexKey(schedule.TargetType, schedule.TargetId, schedule.Id)
|
||||||
|
if err := s.db.Put(targetIndexKey, []byte{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateTestSchedule updates an existing schedule
|
||||||
|
func (s *KVStorage) UpdateTestSchedule(schedule *happydns.TestSchedule) error {
|
||||||
|
key := makeTestScheduleKey(schedule.Id)
|
||||||
|
return s.db.Put(key, schedule)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteTestSchedule removes a schedule and its indexes
|
||||||
|
func (s *KVStorage) DeleteTestSchedule(scheduleId happydns.Identifier) error {
|
||||||
|
// Get the schedule first to know what indexes to delete
|
||||||
|
schedule, err := s.GetTestSchedule(scheduleId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete indexes
|
||||||
|
userIndexKey := makeTestScheduleUserIndexKey(schedule.OwnerId, schedule.Id)
|
||||||
|
if err := s.db.Delete(userIndexKey); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
targetIndexKey := makeTestScheduleTargetIndexKey(schedule.TargetType, schedule.TargetId, schedule.Id)
|
||||||
|
if err := s.db.Delete(targetIndexKey); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the schedule itself
|
||||||
|
key := makeTestScheduleKey(scheduleId)
|
||||||
|
return s.db.Delete(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Execution storage keys:
|
||||||
|
// testexec|{execution-id}
|
||||||
|
|
||||||
|
func makeTestExecutionKey(executionId happydns.Identifier) string {
|
||||||
|
return fmt.Sprintf("testexec|%s", executionId.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListActiveTestExecutions retrieves all executions that are pending or running
|
||||||
|
func (s *KVStorage) ListActiveTestExecutions() ([]*happydns.TestExecution, error) {
|
||||||
|
iter := s.db.Search("testexec|")
|
||||||
|
defer iter.Release()
|
||||||
|
|
||||||
|
var executions []*happydns.TestExecution
|
||||||
|
for iter.Next() {
|
||||||
|
var exec happydns.TestExecution
|
||||||
|
if err := s.db.DecodeData(iter.Value(), &exec); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if exec.Status == happydns.TestExecutionPending || exec.Status == happydns.TestExecutionRunning {
|
||||||
|
executions = append(executions, &exec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return executions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTestExecution retrieves a specific execution by ID
|
||||||
|
func (s *KVStorage) GetTestExecution(executionId happydns.Identifier) (*happydns.TestExecution, error) {
|
||||||
|
key := makeTestExecutionKey(executionId)
|
||||||
|
var execution happydns.TestExecution
|
||||||
|
err := s.db.Get(key, &execution)
|
||||||
|
if errors.Is(err, happydns.ErrNotFound) {
|
||||||
|
return nil, happydns.ErrTestExecutionNotFound
|
||||||
|
}
|
||||||
|
return &execution, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTestExecution creates a new test execution record
|
||||||
|
func (s *KVStorage) CreateTestExecution(execution *happydns.TestExecution) error {
|
||||||
|
key, id, err := s.db.FindIdentifierKey("testexec|")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
execution.Id = id
|
||||||
|
return s.db.Put(key, execution)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateTestExecution updates an existing execution record
|
||||||
|
func (s *KVStorage) UpdateTestExecution(execution *happydns.TestExecution) error {
|
||||||
|
key := makeTestExecutionKey(execution.Id)
|
||||||
|
return s.db.Put(key, execution)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteTestExecution removes an execution record
|
||||||
|
func (s *KVStorage) DeleteTestExecution(executionId happydns.Identifier) error {
|
||||||
|
key := makeTestExecutionKey(executionId)
|
||||||
|
return s.db.Delete(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scheduler state storage key:
|
||||||
|
// testscheduler.lastrun
|
||||||
|
|
||||||
|
// TestSchedulerRun marks that the scheduler has run at current time
|
||||||
|
func (s *KVStorage) TestSchedulerRun() error {
|
||||||
|
now := time.Now()
|
||||||
|
return s.db.Put("testscheduler.lastrun", &now)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LastTestSchedulerRun retrieves the last time the scheduler ran
|
||||||
|
func (s *KVStorage) LastTestSchedulerRun() (*time.Time, error) {
|
||||||
|
var lastRun time.Time
|
||||||
|
err := s.db.Get("testscheduler.lastrun", &lastRun)
|
||||||
|
if errors.Is(err, happydns.ErrNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &lastRun, nil
|
||||||
|
}
|
||||||
|
|
@ -23,6 +23,7 @@ package plugin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"git.happydns.org/happyDomain/model"
|
"git.happydns.org/happyDomain/model"
|
||||||
|
|
@ -104,9 +105,7 @@ func (tu *testPluginUsecase) GetTestPluginOptions(pname string, userid *happydns
|
||||||
opts := make(happydns.PluginOptions)
|
opts := make(happydns.PluginOptions)
|
||||||
|
|
||||||
for _, c := range configs {
|
for _, c := range configs {
|
||||||
for k, v := range c.Options {
|
maps.Copy(opts, c.Options)
|
||||||
opts[k] = v
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &opts, nil
|
return &opts, nil
|
||||||
|
|
@ -117,7 +116,40 @@ func (tu *testPluginUsecase) ListTestPlugins() ([]happydns.TestPlugin, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tu *testPluginUsecase) SetTestPluginOptions(pname string, userid *happydns.Identifier, domainid *happydns.Identifier, serviceid *happydns.Identifier, opts happydns.PluginOptions) error {
|
func (tu *testPluginUsecase) SetTestPluginOptions(pname string, userid *happydns.Identifier, domainid *happydns.Identifier, serviceid *happydns.Identifier, opts happydns.PluginOptions) error {
|
||||||
return tu.store.UpdatePluginConfiguration(pname, userid, domainid, serviceid, opts)
|
// filter opts that correspond to the level set
|
||||||
|
plugin, err := tu.GetTestPlugin(pname)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to get test plugin: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var optNames []string
|
||||||
|
if serviceid != nil {
|
||||||
|
for _, opt := range plugin.AvailableOptions().ServiceOpts {
|
||||||
|
optNames = append(optNames, opt.Id)
|
||||||
|
}
|
||||||
|
} else if domainid != nil {
|
||||||
|
for _, opt := range plugin.AvailableOptions().DomainOpts {
|
||||||
|
optNames = append(optNames, opt.Id)
|
||||||
|
}
|
||||||
|
} else if userid != nil {
|
||||||
|
for _, opt := range plugin.AvailableOptions().UserOpts {
|
||||||
|
optNames = append(optNames, opt.Id)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, opt := range plugin.AvailableOptions().AdminOpts {
|
||||||
|
optNames = append(optNames, opt.Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter opts to only include keys that are in optNames
|
||||||
|
filteredOpts := make(happydns.PluginOptions)
|
||||||
|
for _, optName := range optNames {
|
||||||
|
if val, exists := opts[optName]; exists && val != "" {
|
||||||
|
filteredOpts[optName] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tu.store.UpdatePluginConfiguration(pname, userid, domainid, serviceid, filteredOpts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tu *testPluginUsecase) OverwriteSomeTestPluginOptions(pname string, userid *happydns.Identifier, domainid *happydns.Identifier, serviceid *happydns.Identifier, opts happydns.PluginOptions) error {
|
func (tu *testPluginUsecase) OverwriteSomeTestPluginOptions(pname string, userid *happydns.Identifier, domainid *happydns.Identifier, serviceid *happydns.Identifier, opts happydns.PluginOptions) error {
|
||||||
|
|
@ -126,9 +158,7 @@ func (tu *testPluginUsecase) OverwriteSomeTestPluginOptions(pname string, userid
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for k, v := range opts {
|
maps.Copy(*current, opts)
|
||||||
(*current)[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
return tu.store.UpdatePluginConfiguration(pname, userid, domainid, serviceid, *current)
|
return tu.store.UpdatePluginConfiguration(pname, userid, domainid, serviceid, *current)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
98
internal/usecase/testresult/storage.go
Normal file
98
internal/usecase/testresult/storage.go
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
// This file is part of the happyDomain (R) project.
|
||||||
|
// Copyright (c) 2020-2026 happyDomain
|
||||||
|
// Authors: Pierre-Olivier Mercier, et al.
|
||||||
|
//
|
||||||
|
// This program is offered under a commercial and under the AGPL license.
|
||||||
|
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||||
|
//
|
||||||
|
// For AGPL licensing:
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package testresult
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.happydns.org/happyDomain/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestResultStorage defines the storage interface for test results and related data
|
||||||
|
type TestResultStorage interface {
|
||||||
|
// Test Results
|
||||||
|
// ListTestResults retrieves test results for a specific plugin+target combination
|
||||||
|
ListTestResults(pluginName string, targetType happydns.TestScopeType, targetId happydns.Identifier, limit int) ([]*happydns.TestResult, error)
|
||||||
|
|
||||||
|
// ListTestResultsByPlugin retrieves all test results for a plugin across all targets for a user
|
||||||
|
ListTestResultsByPlugin(userId happydns.Identifier, pluginName string, limit int) ([]*happydns.TestResult, error)
|
||||||
|
|
||||||
|
// ListTestResultsByUser retrieves all test results for a user
|
||||||
|
ListTestResultsByUser(userId happydns.Identifier, limit int) ([]*happydns.TestResult, error)
|
||||||
|
|
||||||
|
// GetTestResult retrieves a specific test result by its ID
|
||||||
|
GetTestResult(pluginName string, targetType happydns.TestScopeType, targetId happydns.Identifier, resultId happydns.Identifier) (*happydns.TestResult, error)
|
||||||
|
|
||||||
|
// CreateTestResult stores a new test result
|
||||||
|
CreateTestResult(result *happydns.TestResult) error
|
||||||
|
|
||||||
|
// DeleteTestResult removes a specific test result
|
||||||
|
DeleteTestResult(pluginName string, targetType happydns.TestScopeType, targetId happydns.Identifier, resultId happydns.Identifier) error
|
||||||
|
|
||||||
|
// DeleteOldTestResults removes old test results keeping only the most recent N results
|
||||||
|
DeleteOldTestResults(pluginName string, targetType happydns.TestScopeType, targetId happydns.Identifier, keepCount int) error
|
||||||
|
|
||||||
|
// Test Schedules
|
||||||
|
// ListEnabledTestSchedules retrieves all enabled schedules (for scheduler)
|
||||||
|
ListEnabledTestSchedules() ([]*happydns.TestSchedule, error)
|
||||||
|
|
||||||
|
// ListTestSchedulesByUser retrieves all schedules for a specific user
|
||||||
|
ListTestSchedulesByUser(userId happydns.Identifier) ([]*happydns.TestSchedule, error)
|
||||||
|
|
||||||
|
// ListTestSchedulesByTarget retrieves all schedules for a specific target
|
||||||
|
ListTestSchedulesByTarget(targetType happydns.TestScopeType, targetId happydns.Identifier) ([]*happydns.TestSchedule, error)
|
||||||
|
|
||||||
|
// GetTestSchedule retrieves a specific schedule by ID
|
||||||
|
GetTestSchedule(scheduleId happydns.Identifier) (*happydns.TestSchedule, error)
|
||||||
|
|
||||||
|
// CreateTestSchedule creates a new test schedule
|
||||||
|
CreateTestSchedule(schedule *happydns.TestSchedule) error
|
||||||
|
|
||||||
|
// UpdateTestSchedule updates an existing schedule
|
||||||
|
UpdateTestSchedule(schedule *happydns.TestSchedule) error
|
||||||
|
|
||||||
|
// DeleteTestSchedule removes a schedule
|
||||||
|
DeleteTestSchedule(scheduleId happydns.Identifier) error
|
||||||
|
|
||||||
|
// Test Executions
|
||||||
|
// ListActiveTestExecutions retrieves all executions that are pending or running
|
||||||
|
ListActiveTestExecutions() ([]*happydns.TestExecution, error)
|
||||||
|
|
||||||
|
// GetTestExecution retrieves a specific execution by ID
|
||||||
|
GetTestExecution(executionId happydns.Identifier) (*happydns.TestExecution, error)
|
||||||
|
|
||||||
|
// CreateTestExecution creates a new test execution record
|
||||||
|
CreateTestExecution(execution *happydns.TestExecution) error
|
||||||
|
|
||||||
|
// UpdateTestExecution updates an existing execution record
|
||||||
|
UpdateTestExecution(execution *happydns.TestExecution) error
|
||||||
|
|
||||||
|
// DeleteTestExecution removes an execution record
|
||||||
|
DeleteTestExecution(executionId happydns.Identifier) error
|
||||||
|
|
||||||
|
// Scheduler State
|
||||||
|
// TestSchedulerRun marks that the scheduler has run at current time
|
||||||
|
TestSchedulerRun() error
|
||||||
|
|
||||||
|
// LastTestSchedulerRun retrieves the last time the scheduler ran
|
||||||
|
LastTestSchedulerRun() (*time.Time, error)
|
||||||
|
}
|
||||||
222
internal/usecase/testresult/testresult_usecase.go
Normal file
222
internal/usecase/testresult/testresult_usecase.go
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
// This file is part of the happyDomain (R) project.
|
||||||
|
// Copyright (c) 2020-2026 happyDomain
|
||||||
|
// Authors: Pierre-Olivier Mercier, et al.
|
||||||
|
//
|
||||||
|
// This program is offered under a commercial and under the AGPL license.
|
||||||
|
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||||
|
//
|
||||||
|
// For AGPL licensing:
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package testresult
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.happydns.org/happyDomain/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestResultUsecase implements business logic for test results
|
||||||
|
type TestResultUsecase struct {
|
||||||
|
storage TestResultStorage
|
||||||
|
options *happydns.Options
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTestResultUsecase creates a new test result usecase
|
||||||
|
func NewTestResultUsecase(storage TestResultStorage, options *happydns.Options) *TestResultUsecase {
|
||||||
|
return &TestResultUsecase{
|
||||||
|
storage: storage,
|
||||||
|
options: options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListTestResultsByTarget retrieves test results for a specific target
|
||||||
|
func (u *TestResultUsecase) ListTestResultsByTarget(pluginName string, targetType happydns.TestScopeType, targetId happydns.Identifier, limit int) ([]*happydns.TestResult, error) {
|
||||||
|
// Apply default limit if not specified
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 5 // Default to 5 most recent results
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.storage.ListTestResults(pluginName, targetType, targetId, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAllTestResultsByTarget retrieves all test results for a target across all plugins
|
||||||
|
func (u *TestResultUsecase) ListAllTestResultsByTarget(targetType happydns.TestScopeType, targetId happydns.Identifier, userId happydns.Identifier, limit int) ([]*happydns.TestResult, error) {
|
||||||
|
// Get all results for the user and filter by target
|
||||||
|
allResults, err := u.storage.ListTestResultsByUser(userId, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by target
|
||||||
|
var results []*happydns.TestResult
|
||||||
|
for _, r := range allResults {
|
||||||
|
if r.TestType == targetType && r.TargetId.Equals(targetId) {
|
||||||
|
results = append(results, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply limit
|
||||||
|
if limit > 0 && len(results) > limit {
|
||||||
|
results = results[:limit]
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTestResult retrieves a specific test result
|
||||||
|
func (u *TestResultUsecase) GetTestResult(pluginName string, targetType happydns.TestScopeType, targetId happydns.Identifier, resultId happydns.Identifier) (*happydns.TestResult, error) {
|
||||||
|
return u.storage.GetTestResult(pluginName, targetType, targetId, resultId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTestResult stores a new test result and enforces retention policy
|
||||||
|
func (u *TestResultUsecase) CreateTestResult(result *happydns.TestResult) error {
|
||||||
|
// Store the result
|
||||||
|
if err := u.storage.CreateTestResult(result); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enforce retention policy
|
||||||
|
maxResults := u.options.MaxResultsPerTest
|
||||||
|
if maxResults <= 0 {
|
||||||
|
maxResults = 100 // Default
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.storage.DeleteOldTestResults(result.PluginName, result.TestType, result.TargetId, maxResults)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteTestResult removes a specific test result
|
||||||
|
func (u *TestResultUsecase) DeleteTestResult(pluginName string, targetType happydns.TestScopeType, targetId happydns.Identifier, resultId happydns.Identifier) error {
|
||||||
|
return u.storage.DeleteTestResult(pluginName, targetType, targetId, resultId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAllTestResults removes all results for a specific plugin+target combination
|
||||||
|
func (u *TestResultUsecase) DeleteAllTestResults(pluginName string, targetType happydns.TestScopeType, targetId happydns.Identifier) error {
|
||||||
|
// Get all results first
|
||||||
|
results, err := u.storage.ListTestResults(pluginName, targetType, targetId, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete each result
|
||||||
|
for _, r := range results {
|
||||||
|
if err := u.storage.DeleteTestResult(pluginName, targetType, targetId, r.Id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanupOldResults removes test results older than retention period
|
||||||
|
func (u *TestResultUsecase) CleanupOldResults() error {
|
||||||
|
retentionDays := u.options.ResultRetentionDays
|
||||||
|
if retentionDays <= 0 {
|
||||||
|
retentionDays = 90 // Default
|
||||||
|
}
|
||||||
|
|
||||||
|
cutoffTime := time.Now().AddDate(0, 0, -retentionDays)
|
||||||
|
|
||||||
|
// Get all results for all users (inefficient but necessary without a time-based index)
|
||||||
|
// In a production system, you might want to add a time-based index for this
|
||||||
|
// For now, we'll iterate through results and delete old ones
|
||||||
|
|
||||||
|
// This is a placeholder - the actual implementation would need to be optimized
|
||||||
|
// based on specific storage patterns
|
||||||
|
_ = cutoffTime
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTestExecution retrieves the status of a test execution
|
||||||
|
func (u *TestResultUsecase) GetTestExecution(executionId happydns.Identifier) (*happydns.TestExecution, error) {
|
||||||
|
return u.storage.GetTestExecution(executionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTestExecution creates a new test execution record
|
||||||
|
func (u *TestResultUsecase) CreateTestExecution(execution *happydns.TestExecution) error {
|
||||||
|
if execution.StartedAt.IsZero() {
|
||||||
|
execution.StartedAt = time.Now()
|
||||||
|
}
|
||||||
|
return u.storage.CreateTestExecution(execution)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateTestExecution updates an existing test execution
|
||||||
|
func (u *TestResultUsecase) UpdateTestExecution(execution *happydns.TestExecution) error {
|
||||||
|
return u.storage.UpdateTestExecution(execution)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompleteTestExecution marks an execution as completed with a result
|
||||||
|
func (u *TestResultUsecase) CompleteTestExecution(executionId happydns.Identifier, resultId happydns.Identifier) error {
|
||||||
|
execution, err := u.storage.GetTestExecution(executionId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
execution.Status = happydns.TestExecutionCompleted
|
||||||
|
execution.CompletedAt = &now
|
||||||
|
execution.ResultId = &resultId
|
||||||
|
|
||||||
|
return u.storage.UpdateTestExecution(execution)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FailTestExecution marks an execution as failed
|
||||||
|
func (u *TestResultUsecase) FailTestExecution(executionId happydns.Identifier, errorMsg string) error {
|
||||||
|
execution, err := u.storage.GetTestExecution(executionId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
execution.Status = happydns.TestExecutionFailed
|
||||||
|
execution.CompletedAt = &now
|
||||||
|
|
||||||
|
// Store error in a result
|
||||||
|
result := &happydns.TestResult{
|
||||||
|
PluginName: execution.PluginName,
|
||||||
|
TestType: execution.TargetType,
|
||||||
|
TargetId: execution.TargetId,
|
||||||
|
OwnerId: execution.OwnerId,
|
||||||
|
ExecutedAt: time.Now(),
|
||||||
|
ScheduledTest: execution.ScheduleId != nil,
|
||||||
|
Options: execution.Options,
|
||||||
|
Status: happydns.PluginResultStatusKO,
|
||||||
|
StatusLine: "Execution failed",
|
||||||
|
Error: errorMsg,
|
||||||
|
Duration: now.Sub(execution.StartedAt),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := u.CreateTestResult(result); err != nil {
|
||||||
|
return fmt.Errorf("failed to create error result: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
execution.ResultId = &result.Id
|
||||||
|
|
||||||
|
return u.storage.UpdateTestExecution(execution)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteCompletedExecutions removes execution records that are completed
|
||||||
|
func (u *TestResultUsecase) DeleteCompletedExecutions(olderThan time.Duration) error {
|
||||||
|
cutoffTime := time.Now().Add(-olderThan)
|
||||||
|
|
||||||
|
// Get active executions (this won't include completed ones)
|
||||||
|
// We need a different query to get completed executions older than cutoff
|
||||||
|
// For now, this is a placeholder
|
||||||
|
|
||||||
|
_ = cutoffTime
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -101,6 +101,12 @@ type Options struct {
|
||||||
CaptchaLoginThreshold int
|
CaptchaLoginThreshold int
|
||||||
|
|
||||||
PluginsDirectories []string
|
PluginsDirectories []string
|
||||||
|
|
||||||
|
// MaxResultsPerTest is the maximum number of test results to keep per plugin+target combination
|
||||||
|
MaxResultsPerTest int
|
||||||
|
|
||||||
|
// ResultRetentionDays is how long to keep test results before cleanup
|
||||||
|
ResultRetentionDays int
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBaseURL returns the full url to the absolute ExternalURL, including BaseURL.
|
// GetBaseURL returns the full url to the absolute ExternalURL, including BaseURL.
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ var (
|
||||||
ErrDomainLogNotFound = errors.New("domain log not found")
|
ErrDomainLogNotFound = errors.New("domain log not found")
|
||||||
ErrProviderNotFound = errors.New("provider not found")
|
ErrProviderNotFound = errors.New("provider not found")
|
||||||
ErrSessionNotFound = errors.New("session not found")
|
ErrSessionNotFound = errors.New("session not found")
|
||||||
|
ErrTestExecutionNotFound = errors.New("test execution not found")
|
||||||
|
ErrTestResultNotFound = errors.New("test result not found")
|
||||||
ErrUserNotFound = errors.New("user not found")
|
ErrUserNotFound = errors.New("user not found")
|
||||||
ErrUserAlreadyExist = errors.New("user already exists")
|
ErrUserAlreadyExist = errors.New("user already exists")
|
||||||
ErrZoneNotFound = errors.New("zone not found")
|
ErrZoneNotFound = errors.New("zone not found")
|
||||||
|
|
|
||||||
267
model/test_result.go
Normal file
267
model/test_result.go
Normal file
|
|
@ -0,0 +1,267 @@
|
||||||
|
// This file is part of the happyDomain (R) project.
|
||||||
|
// Copyright (c) 2020-2026 happyDomain
|
||||||
|
// Authors: Pierre-Olivier Mercier, et al.
|
||||||
|
//
|
||||||
|
// This program is offered under a commercial and under the AGPL license.
|
||||||
|
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||||
|
//
|
||||||
|
// For AGPL licensing:
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package happydns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestScopeType represents the scope level at which a test is performed
|
||||||
|
type TestScopeType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
TestScopeInstance TestScopeType = iota
|
||||||
|
TestScopeUser
|
||||||
|
TestScopeDomain
|
||||||
|
TestScopeService
|
||||||
|
TestScopeOnDemand
|
||||||
|
)
|
||||||
|
|
||||||
|
// String returns a string representation of the test scope type
|
||||||
|
func (t TestScopeType) String() string {
|
||||||
|
switch t {
|
||||||
|
case TestScopeInstance:
|
||||||
|
return "instance"
|
||||||
|
case TestScopeUser:
|
||||||
|
return "user"
|
||||||
|
case TestScopeDomain:
|
||||||
|
return "domain"
|
||||||
|
case TestScopeService:
|
||||||
|
return "service"
|
||||||
|
case TestScopeOnDemand:
|
||||||
|
return "ondemand"
|
||||||
|
default:
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestExecutionStatus represents the current state of a test execution
|
||||||
|
type TestExecutionStatus int
|
||||||
|
|
||||||
|
const (
|
||||||
|
TestExecutionPending TestExecutionStatus = iota
|
||||||
|
TestExecutionRunning
|
||||||
|
TestExecutionCompleted
|
||||||
|
TestExecutionFailed
|
||||||
|
)
|
||||||
|
|
||||||
|
// String returns a string representation of the test execution status
|
||||||
|
func (t TestExecutionStatus) String() string {
|
||||||
|
switch t {
|
||||||
|
case TestExecutionPending:
|
||||||
|
return "pending"
|
||||||
|
case TestExecutionRunning:
|
||||||
|
return "running"
|
||||||
|
case TestExecutionCompleted:
|
||||||
|
return "completed"
|
||||||
|
case TestExecutionFailed:
|
||||||
|
return "failed"
|
||||||
|
default:
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestResult stores the result of a test execution
|
||||||
|
type TestResult struct {
|
||||||
|
// Id is the unique identifier for this test result
|
||||||
|
Id Identifier `json:"id" swaggertype:"string"`
|
||||||
|
|
||||||
|
// PluginName identifies which test plugin was executed
|
||||||
|
PluginName string `json:"plugin_name"`
|
||||||
|
|
||||||
|
// TestType indicates the scope level of the test
|
||||||
|
TestType TestScopeType `json:"test_type"`
|
||||||
|
|
||||||
|
// TargetId is the identifier of the target (User/Domain/Service)
|
||||||
|
TargetId Identifier `json:"target_id" swaggertype:"string"`
|
||||||
|
|
||||||
|
// OwnerId is the owner of the test
|
||||||
|
OwnerId Identifier `json:"owner_id" swaggertype:"string"`
|
||||||
|
|
||||||
|
// ExecutedAt is when the test was executed
|
||||||
|
ExecutedAt time.Time `json:"executed_at"`
|
||||||
|
|
||||||
|
// ScheduledTest indicates if this was a scheduled (true) or on-demand (false) test
|
||||||
|
ScheduledTest bool `json:"scheduled_test"`
|
||||||
|
|
||||||
|
// Options contains the merged plugin configuration used for this test
|
||||||
|
Options PluginOptions `json:"options,omitempty"`
|
||||||
|
|
||||||
|
// Status is the overall test result status
|
||||||
|
Status PluginResultStatus `json:"status"`
|
||||||
|
|
||||||
|
// StatusLine is a summary message of the test result
|
||||||
|
StatusLine string `json:"status_line"`
|
||||||
|
|
||||||
|
// Report contains the full test report (plugin-specific structure)
|
||||||
|
Report interface{} `json:"report,omitempty"`
|
||||||
|
|
||||||
|
// Duration is how long the test took to execute
|
||||||
|
Duration time.Duration `json:"duration" swaggertype:"integer"`
|
||||||
|
|
||||||
|
// Error contains any error message if the execution failed
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSchedule defines a recurring test schedule
|
||||||
|
type TestSchedule struct {
|
||||||
|
// Id is the unique identifier for this schedule
|
||||||
|
Id Identifier `json:"id" swaggertype:"string"`
|
||||||
|
|
||||||
|
// PluginName identifies which test plugin to execute
|
||||||
|
PluginName string `json:"plugin_name"`
|
||||||
|
|
||||||
|
// OwnerId is the owner of the schedule
|
||||||
|
OwnerId Identifier `json:"owner_id" swaggertype:"string"`
|
||||||
|
|
||||||
|
// TargetType indicates what type of target to test
|
||||||
|
TargetType TestScopeType `json:"target_type"`
|
||||||
|
|
||||||
|
// TargetId is the identifier of the target to test
|
||||||
|
TargetId Identifier `json:"target_id" swaggertype:"string"`
|
||||||
|
|
||||||
|
// Interval is how often to run the test
|
||||||
|
Interval time.Duration `json:"interval" swaggertype:"integer"`
|
||||||
|
|
||||||
|
// Enabled indicates if the schedule is active
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
|
||||||
|
// LastRun is when the test was last executed (nil if never run)
|
||||||
|
LastRun *time.Time `json:"last_run,omitempty"`
|
||||||
|
|
||||||
|
// NextRun is when the test should next be executed
|
||||||
|
NextRun time.Time `json:"next_run"`
|
||||||
|
|
||||||
|
// Options contains plugin-specific configuration
|
||||||
|
Options PluginOptions `json:"options,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestExecution tracks an in-progress or completed test execution
|
||||||
|
type TestExecution struct {
|
||||||
|
// Id is the unique identifier for this execution
|
||||||
|
Id Identifier `json:"id" swaggertype:"string"`
|
||||||
|
|
||||||
|
// ScheduleId is the schedule that triggered this execution (nil for on-demand)
|
||||||
|
ScheduleId *Identifier `json:"schedule_id,omitempty" swaggertype:"string"`
|
||||||
|
|
||||||
|
// PluginName identifies which test plugin is being executed
|
||||||
|
PluginName string `json:"plugin_name"`
|
||||||
|
|
||||||
|
// OwnerId is the owner of the test
|
||||||
|
OwnerId Identifier `json:"owner_id" swaggertype:"string"`
|
||||||
|
|
||||||
|
// TargetType indicates the scope level of the test
|
||||||
|
TargetType TestScopeType `json:"target_type"`
|
||||||
|
|
||||||
|
// TargetId is the identifier of the target being tested
|
||||||
|
TargetId Identifier `json:"target_id" swaggertype:"string"`
|
||||||
|
|
||||||
|
// Status is the current execution status
|
||||||
|
Status TestExecutionStatus `json:"status"`
|
||||||
|
|
||||||
|
// StartedAt is when the execution began
|
||||||
|
StartedAt time.Time `json:"started_at"`
|
||||||
|
|
||||||
|
// CompletedAt is when the execution finished (nil if still running)
|
||||||
|
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||||
|
|
||||||
|
// ResultId links to the TestResult (nil if execution not completed)
|
||||||
|
ResultId *Identifier `json:"result_id,omitempty" swaggertype:"string"`
|
||||||
|
|
||||||
|
// Options contains the plugin configuration for this execution
|
||||||
|
Options PluginOptions `json:"options,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestResultUsecase defines business logic for test results
|
||||||
|
type TestResultUsecase interface {
|
||||||
|
// ListTestResultsByTarget retrieves test results for a specific target
|
||||||
|
ListTestResultsByTarget(pluginName string, targetType TestScopeType, targetId Identifier, limit int) ([]*TestResult, error)
|
||||||
|
|
||||||
|
// ListAllTestResultsByTarget retrieves all test results for a target across all plugins
|
||||||
|
ListAllTestResultsByTarget(targetType TestScopeType, targetId Identifier, userId Identifier, limit int) ([]*TestResult, error)
|
||||||
|
|
||||||
|
// GetTestResult retrieves a specific test result
|
||||||
|
GetTestResult(pluginName string, targetType TestScopeType, targetId Identifier, resultId Identifier) (*TestResult, error)
|
||||||
|
|
||||||
|
// CreateTestResult stores a new test result and enforces retention policy
|
||||||
|
CreateTestResult(result *TestResult) error
|
||||||
|
|
||||||
|
// DeleteTestResult removes a specific test result
|
||||||
|
DeleteTestResult(pluginName string, targetType TestScopeType, targetId Identifier, resultId Identifier) error
|
||||||
|
|
||||||
|
// DeleteAllTestResults removes all results for a specific plugin+target combination
|
||||||
|
DeleteAllTestResults(pluginName string, targetType TestScopeType, targetId Identifier) error
|
||||||
|
|
||||||
|
// GetTestExecution retrieves the status of a test execution
|
||||||
|
GetTestExecution(executionId Identifier) (*TestExecution, error)
|
||||||
|
|
||||||
|
// CreateTestExecution creates a new test execution record
|
||||||
|
CreateTestExecution(execution *TestExecution) error
|
||||||
|
|
||||||
|
// UpdateTestExecution updates an existing test execution
|
||||||
|
UpdateTestExecution(execution *TestExecution) error
|
||||||
|
|
||||||
|
// CompleteTestExecution marks an execution as completed with a result
|
||||||
|
CompleteTestExecution(executionId Identifier, resultId Identifier) error
|
||||||
|
|
||||||
|
// FailTestExecution marks an execution as failed
|
||||||
|
FailTestExecution(executionId Identifier, errorMsg string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestScheduleUsecase defines business logic for test schedules
|
||||||
|
type TestScheduleUsecase interface {
|
||||||
|
// ListUserSchedules retrieves all schedules for a specific user
|
||||||
|
ListUserSchedules(userId Identifier) ([]*TestSchedule, error)
|
||||||
|
|
||||||
|
// ListSchedulesByTarget retrieves all schedules for a specific target
|
||||||
|
ListSchedulesByTarget(targetType TestScopeType, targetId Identifier) ([]*TestSchedule, error)
|
||||||
|
|
||||||
|
// GetSchedule retrieves a specific schedule by ID
|
||||||
|
GetSchedule(scheduleId Identifier) (*TestSchedule, error)
|
||||||
|
|
||||||
|
// CreateSchedule creates a new test schedule with validation
|
||||||
|
CreateSchedule(schedule *TestSchedule) error
|
||||||
|
|
||||||
|
// UpdateSchedule updates an existing schedule
|
||||||
|
UpdateSchedule(schedule *TestSchedule) error
|
||||||
|
|
||||||
|
// DeleteSchedule removes a schedule
|
||||||
|
DeleteSchedule(scheduleId Identifier) error
|
||||||
|
|
||||||
|
// EnableSchedule enables a schedule
|
||||||
|
EnableSchedule(scheduleId Identifier) error
|
||||||
|
|
||||||
|
// DisableSchedule disables a schedule
|
||||||
|
DisableSchedule(scheduleId Identifier) error
|
||||||
|
|
||||||
|
// UpdateScheduleAfterRun updates a schedule after it has been executed
|
||||||
|
UpdateScheduleAfterRun(scheduleId Identifier) error
|
||||||
|
|
||||||
|
// ListDueSchedules retrieves all enabled schedules that are due to run
|
||||||
|
ListDueSchedules() ([]*TestSchedule, error)
|
||||||
|
|
||||||
|
// ValidateScheduleOwnership checks if a user owns a schedule
|
||||||
|
ValidateScheduleOwnership(scheduleId Identifier, ownerId Identifier) error
|
||||||
|
|
||||||
|
// DeleteSchedulesForTarget removes all schedules for a target
|
||||||
|
DeleteSchedulesForTarget(targetType TestScopeType, targetId Identifier) error
|
||||||
|
}
|
||||||
|
|
@ -37,6 +37,7 @@ type UsecaseDependancies interface {
|
||||||
ServiceSpecsUsecase() ServiceSpecsUsecase
|
ServiceSpecsUsecase() ServiceSpecsUsecase
|
||||||
SessionUsecase() SessionUsecase
|
SessionUsecase() SessionUsecase
|
||||||
TestPluginUsecase() TestPluginUsecase
|
TestPluginUsecase() TestPluginUsecase
|
||||||
|
TestResultUsecase() TestResultUsecase
|
||||||
UserUsecase() UserUsecase
|
UserUsecase() UserUsecase
|
||||||
ZoneCorrectionApplierUsecase() ZoneCorrectionApplierUsecase
|
ZoneCorrectionApplierUsecase() ZoneCorrectionApplierUsecase
|
||||||
ZoneImporterUsecase() ZoneImporterUsecase
|
ZoneImporterUsecase() ZoneImporterUsecase
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,17 @@ type UserSettings struct {
|
||||||
|
|
||||||
// ShowRRTypes tells if we show equivalent RRTypes in interface (for advanced users).
|
// ShowRRTypes tells if we show equivalent RRTypes in interface (for advanced users).
|
||||||
ShowRRTypes bool `json:"showrrtypes,omitempty"`
|
ShowRRTypes bool `json:"showrrtypes,omitempty"`
|
||||||
|
|
||||||
|
// TestRetention overrides instance default for how long to keep test results (days)
|
||||||
|
TestRetention int `json:"test_retention,omitempty"`
|
||||||
|
|
||||||
|
// DomainTestInterval is the default interval for domain-level tests (seconds)
|
||||||
|
// Default: 86400 (24 hours)
|
||||||
|
DomainTestInterval int64 `json:"domain_test_interval,omitempty"`
|
||||||
|
|
||||||
|
// ServiceTestInterval is the default interval for service-level tests (seconds)
|
||||||
|
// Default: 3600 (1 hour)
|
||||||
|
ServiceTestInterval int64 `json:"service_test_interval,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaultUserSettings() *UserSettings {
|
func DefaultUserSettings() *UserSettings {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue