mirror of
https://github.com/aptly-dev/aptly.git
synced 2026-06-19 07:40:20 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8fa1922477 | |||
| 836137f15d |
+230
-1265
File diff suppressed because it is too large
Load Diff
@@ -44,7 +44,7 @@ jobs:
|
||||
# Require: The version of golangci-lint to use.
|
||||
# When `install-mode` is `binary` (default) the value can be v1.2 or v1.2.3 or `latest` to use the latest version.
|
||||
# When `install-mode` is `goinstall` the value can be v1.2.3, `latest`, or the hash of a commit.
|
||||
version: v1.64.5
|
||||
version: v1.54.1
|
||||
|
||||
# Optional: working directory, useful for monorepos
|
||||
# working-directory: somedir
|
||||
|
||||
+1
-39
@@ -38,6 +38,7 @@ man/aptly.1.ronn
|
||||
system/env/
|
||||
|
||||
# created by make build for release artifacts
|
||||
VERSION
|
||||
aptly.test
|
||||
|
||||
build/
|
||||
@@ -73,42 +74,3 @@ docs/docs.go
|
||||
docs/swagger.json
|
||||
docs/swagger.yaml
|
||||
docs/swagger.conf
|
||||
.secrets
|
||||
|
||||
# Coverage reports
|
||||
*.out
|
||||
coverage.html
|
||||
*_coverage.html
|
||||
|
||||
# Binaries
|
||||
aptly-binary
|
||||
aptly-test
|
||||
|
||||
# Downloaded archives
|
||||
*.tar.gz
|
||||
|
||||
# Test artifacts
|
||||
test_results.log
|
||||
|
||||
# Python virtual environments
|
||||
system/venv/
|
||||
venv/
|
||||
|
||||
# act local CI runner
|
||||
.actrc
|
||||
|
||||
# Backup files
|
||||
*.backup
|
||||
*.bak
|
||||
|
||||
# Temporary directories
|
||||
coverage/
|
||||
scripts/
|
||||
|
||||
# Binary executables
|
||||
aptly/aptly
|
||||
|
||||
# Coverage reports
|
||||
coverage_report.html
|
||||
*.coverage
|
||||
coverage.out
|
||||
|
||||
+11
-206
@@ -1,211 +1,16 @@
|
||||
# golangci-lint configuration for aptly
|
||||
# Run with: golangci-lint run
|
||||
|
||||
run:
|
||||
# Timeout for analysis
|
||||
timeout: 5m
|
||||
|
||||
# Include test files
|
||||
tests: true
|
||||
tests: false
|
||||
|
||||
output:
|
||||
# Format of output
|
||||
formats:
|
||||
- format: colored-line-number
|
||||
|
||||
# Print lines of code with issue
|
||||
print-issued-lines: true
|
||||
|
||||
# Print linter name in the end of issue text
|
||||
print-linter-name: true
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
# Default linters
|
||||
- errcheck # Check for unchecked errors
|
||||
- gosimple # Simplify code
|
||||
- govet # Go vet
|
||||
- ineffassign # Detect ineffectual assignments
|
||||
- staticcheck # Static analysis
|
||||
- typecheck # Type checking
|
||||
- unused # Find unused code
|
||||
|
||||
# Additional linters for code quality
|
||||
- bodyclose # Check HTTP response body is closed
|
||||
- dupl # Code duplication
|
||||
- copyloopvar # Check loop variable export (replacement for exportloopref)
|
||||
- gocognit # Cognitive complexity
|
||||
- gocritic # Opinionated linter
|
||||
- gocyclo # Cyclomatic complexity
|
||||
- gofmt # Formatting
|
||||
- goimports # Import formatting
|
||||
- revive # Fast, configurable linter
|
||||
- unconvert # Unnecessary type conversions
|
||||
- unparam # Unused function parameters
|
||||
- gosec # Security issues
|
||||
- prealloc # Preallocate slices
|
||||
- predeclared # Shadowing of predeclared identifiers
|
||||
- makezero # Make slice with non-zero length
|
||||
- nakedret # Naked returns in long functions
|
||||
|
||||
disable:
|
||||
# Disabled because they're too strict or noisy
|
||||
- exhaustive # Too strict for switch statements
|
||||
- wsl # Whitespace linter (too opinionated)
|
||||
- godox # TODO/FIXME comments
|
||||
- gochecknoglobals # We use some globals
|
||||
- gochecknoinits # We use init functions
|
||||
|
||||
linters-settings:
|
||||
# errcheck
|
||||
errcheck:
|
||||
# Report about not checking of errors in type assertions
|
||||
check-type-assertions: true
|
||||
# Report about assignment of errors to blank identifier
|
||||
check-blank: true
|
||||
# Exclude some functions from checking
|
||||
exclude-functions:
|
||||
- io/ioutil.ReadFile
|
||||
- io.Copy(*bytes.Buffer)
|
||||
- io.Copy(os.Stdout)
|
||||
|
||||
# govet
|
||||
govet:
|
||||
enable-all: true
|
||||
disable:
|
||||
- fieldalignment # Too many false positives
|
||||
|
||||
# gocyclo
|
||||
gocyclo:
|
||||
min-complexity: 15
|
||||
|
||||
# gocognit
|
||||
gocognit:
|
||||
min-complexity: 20
|
||||
|
||||
# dupl
|
||||
dupl:
|
||||
threshold: 200
|
||||
|
||||
# gocritic
|
||||
gocritic:
|
||||
enabled-tags:
|
||||
- diagnostic
|
||||
- performance
|
||||
- style
|
||||
disabled-checks:
|
||||
- commentedOutCode
|
||||
- whyNoLint
|
||||
|
||||
# gosec
|
||||
gosec:
|
||||
severity: low
|
||||
confidence: low
|
||||
excludes:
|
||||
- G404 # Weak random for non-crypto use is ok
|
||||
|
||||
# revive
|
||||
revive:
|
||||
rules:
|
||||
- name: blank-imports
|
||||
- name: context-as-argument
|
||||
- name: context-keys-type
|
||||
- name: dot-imports
|
||||
- name: empty-block
|
||||
- name: error-naming
|
||||
- name: error-return
|
||||
- name: error-strings
|
||||
- name: errorf
|
||||
- name: exported
|
||||
- name: increment-decrement
|
||||
- name: indent-error-flow
|
||||
- name: package-comments
|
||||
- name: range
|
||||
- name: receiver-naming
|
||||
- name: time-naming
|
||||
- name: unexported-return
|
||||
- name: var-declaration
|
||||
- name: var-naming
|
||||
|
||||
# goimports
|
||||
goimports:
|
||||
local-prefixes: github.com/aptly-dev/aptly
|
||||
|
||||
# gofmt
|
||||
gofmt:
|
||||
simplify: true
|
||||
|
||||
# unparam
|
||||
unparam:
|
||||
check-exported: false
|
||||
|
||||
# nakedret
|
||||
nakedret:
|
||||
max-func-lines: 30
|
||||
|
||||
# prealloc
|
||||
prealloc:
|
||||
simple: true
|
||||
range-loops: true
|
||||
for-loops: false
|
||||
|
||||
issues:
|
||||
# Maximum issues count per one linter
|
||||
max-issues-per-linter: 0
|
||||
|
||||
# Maximum count of issues with the same text
|
||||
max-same-issues: 0
|
||||
|
||||
# Skip directories
|
||||
exclude-dirs:
|
||||
- vendor
|
||||
- testdata
|
||||
- system/files
|
||||
|
||||
# Skip files matching these patterns
|
||||
exclude-files:
|
||||
- ".*\\.pb\\.go$"
|
||||
- ".*\\.gen\\.go$"
|
||||
|
||||
# Exclude some linters from running on tests files
|
||||
exclude-rules:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- dupl
|
||||
- gosec
|
||||
- gocognit
|
||||
- gocyclo
|
||||
|
||||
# Exclude some linters from running on generated files
|
||||
- path: ".*\\.gen\\.go$"
|
||||
linters:
|
||||
- all
|
||||
|
||||
# Exclude known issues in vendor
|
||||
- path: vendor/
|
||||
linters:
|
||||
- all
|
||||
|
||||
# Allow fmt.Printf in main/cmd
|
||||
- path: (cmd|main)\.go
|
||||
linters:
|
||||
- forbidigo
|
||||
|
||||
# Independently from option `exclude` we use default exclude patterns
|
||||
exclude-use-default: true
|
||||
|
||||
# Fix found issues (if it's supported by the linter)
|
||||
fix: false
|
||||
|
||||
severity:
|
||||
# Set the default severity for issues
|
||||
default-severity: warning
|
||||
|
||||
# The list of ids of default excludes to include or disable
|
||||
rules:
|
||||
- linters:
|
||||
- gosec
|
||||
severity: info
|
||||
- linters:
|
||||
- dupl
|
||||
severity: info
|
||||
- goconst
|
||||
- gofmt
|
||||
- goimports
|
||||
- govet
|
||||
- ineffassign
|
||||
- misspell
|
||||
- revive
|
||||
- staticcheck
|
||||
- vetshadow
|
||||
|
||||
@@ -68,4 +68,4 @@ List of contributors, in chronological order:
|
||||
* Blake Kostner (https://github.com/btkostner)
|
||||
* Leigh London (https://github.com/leighlondon)
|
||||
* Gordian Schoenherr (https://github.com/schoenherrg)
|
||||
* Silke Hofstra (https://github.com/silkeh)
|
||||
* Brett Hawn (https://github.com/bpiraeus)
|
||||
|
||||
+1
-197
@@ -158,7 +158,7 @@ This section describes local setup to start contributing to aptly.
|
||||
|
||||
#### Dependencies
|
||||
|
||||
Building aptly requires go version 1.24.
|
||||
Building aptly requires go version 1.22.
|
||||
|
||||
On Debian bookworm with backports enabled, go can be installed with:
|
||||
|
||||
@@ -178,149 +178,6 @@ To install aptly into `$GOPATH/bin`, run:
|
||||
|
||||
make install
|
||||
|
||||
#### Platform-Specific Setup
|
||||
|
||||
##### macOS
|
||||
|
||||
This guide explains how to run aptly tests on macOS, including Apple Silicon (M1/M2) machines.
|
||||
|
||||
###### Prerequisites
|
||||
|
||||
1. **Install Go** (1.24 or later):
|
||||
```bash
|
||||
brew install go
|
||||
```
|
||||
|
||||
2. **Install Docker** (for etcd and other services):
|
||||
```bash
|
||||
brew install --cask docker
|
||||
```
|
||||
|
||||
3. **Install test dependencies**:
|
||||
```bash
|
||||
# Add Go binaries to PATH
|
||||
export PATH=$PATH:~/go/bin
|
||||
|
||||
# Install swag for API documentation
|
||||
go install github.com/swaggo/swag/cmd/swag@latest
|
||||
|
||||
# Install other tools
|
||||
brew install etcd # Optional: for local etcd instead of Docker
|
||||
```
|
||||
|
||||
###### Running Tests on macOS
|
||||
|
||||
**Option 1: Using Docker Compose (Recommended)**
|
||||
|
||||
```bash
|
||||
# Start test services
|
||||
docker-compose -f docker-compose.ci.yml up -d etcd
|
||||
|
||||
# Run tests
|
||||
PATH=$PATH:~/go/bin make test
|
||||
```
|
||||
|
||||
**Option 2: Using Local etcd**
|
||||
|
||||
```bash
|
||||
# Install and start etcd locally
|
||||
brew services start etcd
|
||||
|
||||
# Run tests with local etcd
|
||||
ETCD_ENDPOINTS=localhost:2379 go test ./...
|
||||
```
|
||||
|
||||
**Option 3: Run Specific Test Suites**
|
||||
|
||||
```bash
|
||||
# Fix VERSION file if needed
|
||||
echo "1.5.0" > VERSION
|
||||
|
||||
# Run unit tests only
|
||||
PATH=$PATH:~/go/bin make test-unit GOTEST="go test -short -timeout=5m"
|
||||
|
||||
# Run specific packages
|
||||
go test ./deb ./s3 ./utils ./context -short -v
|
||||
|
||||
# Run with race detection
|
||||
go test -race ./deb ./s3 ./utils -short
|
||||
```
|
||||
|
||||
###### macOS-Specific Considerations
|
||||
|
||||
1. **CPU Architecture**: The install scripts now support both Intel (x86_64) and Apple Silicon (arm64).
|
||||
|
||||
2. **File System**: macOS is case-insensitive by default, which may affect some tests.
|
||||
|
||||
3. **Network**: Some tests may require adjusting firewall settings.
|
||||
|
||||
4. **Timeouts**: Some tests may need longer timeouts on macOS:
|
||||
```bash
|
||||
go test -timeout=10m ./...
|
||||
```
|
||||
|
||||
###### Troubleshooting on macOS
|
||||
|
||||
**etcd Installation Fails**
|
||||
|
||||
If the automatic etcd installation fails, use Docker or Homebrew:
|
||||
```bash
|
||||
# Using Docker
|
||||
docker run -d -p 2379:2379 --name etcd quay.io/coreos/etcd:latest
|
||||
|
||||
# Using Homebrew
|
||||
brew install etcd
|
||||
etcd --listen-client-urls http://0.0.0.0:2379 --advertise-client-urls http://localhost:2379
|
||||
```
|
||||
|
||||
**Test Timeouts**
|
||||
|
||||
Increase timeouts for slower tests:
|
||||
```bash
|
||||
go test -timeout=30m ./...
|
||||
```
|
||||
|
||||
**Race Detector Issues**
|
||||
|
||||
The race detector may be slower on macOS. Disable for faster runs:
|
||||
```bash
|
||||
go test ./... -short
|
||||
```
|
||||
|
||||
###### CI Integration for macOS
|
||||
|
||||
For GitHub Actions on macOS:
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
test-macos:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.24'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
brew install etcd
|
||||
go install github.com/swaggo/swag/cmd/swag@latest
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
export PATH=$PATH:~/go/bin
|
||||
make test
|
||||
```
|
||||
|
||||
###### Test Coverage on macOS
|
||||
|
||||
Generate coverage reports:
|
||||
```bash
|
||||
go test -coverprofile=coverage.out ./...
|
||||
go tool cover -html=coverage.out -o coverage.html
|
||||
open coverage.html
|
||||
```
|
||||
|
||||
#### Unit-tests
|
||||
|
||||
aptly has two kinds of tests: unit-tests and functional (system) tests. Functional tests are preferred way to test any
|
||||
@@ -377,59 +234,6 @@ There are some packages available under `system/files/` directory which are used
|
||||
this default location. You can run aptly under different user or by using non-default config location with non-default
|
||||
aptly root directory.
|
||||
|
||||
### Continuous Integration (CI)
|
||||
|
||||
aptly uses GitHub Actions for continuous integration. The CI pipeline includes:
|
||||
|
||||
- **Quick checks**: Code formatting, go vet, mod tidy, and flake8 linting
|
||||
- **Security scanning**: govulncheck and Trivy vulnerability scanning
|
||||
- **Linting**: golangci-lint with extensive checks
|
||||
- **Unit tests**: With race detection on Go 1.23 and 1.24
|
||||
- **Integration tests**: Full system tests with cloud storage backends
|
||||
- **Benchmarks**: Performance testing
|
||||
- **Extended tests**: Combined unit tests and benchmarks with coverage merging
|
||||
- **Cross-platform builds**: Binaries for Linux, macOS, Windows, FreeBSD (multiple architectures)
|
||||
- **Debian packages**: Built for Debian (buster, bullseye, bookworm, trixie) and Ubuntu (focal, jammy, noble)
|
||||
- **Docker images**: Multi-architecture container images (linux/amd64, linux/arm64)
|
||||
|
||||
All pull requests must pass CI checks before merging. Build artifacts are available for download from GitHub Actions runs with the following retention:
|
||||
- CI builds: 7 days
|
||||
- Tagged releases: 90 days
|
||||
|
||||
#### Testing CI Locally with act
|
||||
|
||||
You can test GitHub Actions workflows locally using [act](https://github.com/nektos/act):
|
||||
|
||||
```bash
|
||||
# Install act
|
||||
brew install act # macOS
|
||||
# or
|
||||
curl https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash # Linux
|
||||
|
||||
# Run default push event
|
||||
act
|
||||
|
||||
# Run pull request event
|
||||
act pull_request
|
||||
|
||||
# Run specific job
|
||||
act -j test-unit
|
||||
|
||||
# Run with specific matrix values
|
||||
act -j test-unit --matrix go:1.24
|
||||
|
||||
# List all available jobs
|
||||
act -l
|
||||
```
|
||||
|
||||
For Apple Silicon Macs, use: `act --container-architecture linux/amd64`
|
||||
|
||||
Common use cases:
|
||||
- Test a job before pushing: `act -j quick-checks`
|
||||
- Test PR workflows: Create a PR event file and run `act pull_request -e pr-event.json`
|
||||
- Debug failures: `act -j failing-job -v` for verbose output
|
||||
- Use secrets: Create `.secrets` file with `KEY=value` format and run `act --secret-file .secrets`
|
||||
|
||||
### man Page
|
||||
|
||||
aptly is using combination of [Go templates](http://godoc.org/text/template) and automatically generated text to build `aptly.1` man page. If either source
|
||||
|
||||
@@ -1,74 +1,26 @@
|
||||
# Modern Makefile for aptly with improved tooling and practices
|
||||
GOPATH=$(shell go env GOPATH)
|
||||
VERSION=$(shell make -s version)
|
||||
PYTHON?=python3
|
||||
BINPATH?=$(GOPATH)/bin
|
||||
GOLANGCI_LINT_VERSION=v1.54.1 # version supporting go 1.19
|
||||
COVERAGE_DIR?=$(shell mktemp -d)
|
||||
GOOS=$(shell go env GOHOSTOS)
|
||||
GOARCH=$(shell go env GOHOSTARCH)
|
||||
|
||||
SHELL := /bin/bash
|
||||
.DEFAULT_GOAL := help
|
||||
.PHONY: help
|
||||
# Uncomment to update system test gold files
|
||||
# CAPTURE := "--capture"
|
||||
|
||||
# Version and build info
|
||||
BUILD_DATE := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||||
help: ## Print this help
|
||||
@grep -E '^[a-zA-Z][a-zA-Z0-9_-]*:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
# Go parameters
|
||||
GOCMD := go
|
||||
GOBUILD := $(GOCMD) build
|
||||
GOTEST := $(GOCMD) test
|
||||
GOGET := $(GOCMD) get
|
||||
GOMOD := $(GOCMD) mod
|
||||
GOFMT := gofmt
|
||||
GOPATH := $(shell go env GOPATH)
|
||||
BINPATH := $(GOPATH)/bin
|
||||
GOOS := $(shell go env GOHOSTOS)
|
||||
GOARCH := $(shell go env GOHOSTARCH)
|
||||
prepare: ## Install go module dependencies
|
||||
# Prepare go modules
|
||||
go mod verify
|
||||
go mod tidy -v
|
||||
# Generate VERSION file
|
||||
go generate
|
||||
|
||||
# OS detection
|
||||
UNAME_S := $(shell uname -s)
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
OS_TYPE := macos
|
||||
else
|
||||
OS_TYPE := linux
|
||||
endif
|
||||
|
||||
# Tool versions
|
||||
GOLANGCI_VERSION := v1.64.5
|
||||
AIR_VERSION := v1.52.3
|
||||
SWAG_VERSION := v1.16.4
|
||||
GOVULNCHECK_VERSION := latest
|
||||
|
||||
# Build parameters
|
||||
BINARY_NAME := aptly
|
||||
BUILD_DIR := build
|
||||
COVERAGE_DIR := coverage
|
||||
COVERAGE_FILE := $(COVERAGE_DIR)/coverage.out
|
||||
|
||||
# Docker parameters
|
||||
DOCKER_IMAGE := aptly/aptly
|
||||
DOCKER_TAG := $(VERSION)
|
||||
|
||||
# Colors for output
|
||||
COLOR_RESET := \033[0m
|
||||
COLOR_BOLD := \033[1m
|
||||
COLOR_GREEN := \033[32m
|
||||
COLOR_YELLOW := \033[33m
|
||||
COLOR_RED := \033[31m
|
||||
COLOR_BLUE := \033[34m
|
||||
|
||||
##@ General
|
||||
|
||||
help: ## Display this help
|
||||
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
|
||||
|
||||
version: ## Show version
|
||||
@ci="" ; \
|
||||
if [ "`make -s releasetype`" = "ci" ]; then \
|
||||
ci=`TZ=UTC git show -s --format='+%cd.%h' --date=format-local:'%Y%m%d%H%M%S'`; \
|
||||
fi ; \
|
||||
if which dpkg-parsechangelog > /dev/null 2>&1; then \
|
||||
echo `dpkg-parsechangelog -S Version`$$ci; \
|
||||
else \
|
||||
echo `grep ^aptly -m1 debian/changelog | sed 's/.*(\([^)]\+\)).*/\1/'`$$ci ; \
|
||||
fi
|
||||
|
||||
releasetype: # Print release type: ci (on any branch/commit), release (on a tag)
|
||||
releasetype: # Print release type: ci (on any branch/commit), release (on a tag)
|
||||
@reltype=ci ; \
|
||||
gitbranch=`git rev-parse --abbrev-ref HEAD` ; \
|
||||
if [ "$$gitbranch" = "HEAD" ] && [ "$$FORCE_CI" != "true" ]; then \
|
||||
@@ -79,236 +31,189 @@ releasetype: # Print release type: ci (on any branch/commit), release (on a tag)
|
||||
fi ; \
|
||||
echo $$reltype
|
||||
|
||||
##@ Development
|
||||
|
||||
prepare: ## Prepare development environment
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Preparing development environment...$(COLOR_RESET)"
|
||||
$(GOMOD) download
|
||||
$(GOMOD) verify
|
||||
$(GOMOD) tidy -v
|
||||
@go generate ./...
|
||||
|
||||
dev-tools: ## Install development tools
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Installing development tools...$(COLOR_RESET)"
|
||||
@go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_VERSION)
|
||||
@go install github.com/air-verse/air@$(AIR_VERSION)
|
||||
@go install github.com/swaggo/swag/cmd/swag@$(SWAG_VERSION)
|
||||
@go install golang.org/x/vuln/cmd/govulncheck@$(GOVULNCHECK_VERSION)
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Development tools installed$(COLOR_RESET)"
|
||||
|
||||
##@ Build
|
||||
|
||||
build: prepare swagger ## Build aptly binary
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Building aptly...$(COLOR_RESET)"
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME) .
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Build complete: $(BUILD_DIR)/$(BINARY_NAME)$(COLOR_RESET)"
|
||||
|
||||
build-all: prepare swagger ## Build for all platforms
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Building for all platforms...$(COLOR_RESET)"
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
# Linux
|
||||
GOOS=linux GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64
|
||||
GOOS=linux GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64
|
||||
# macOS
|
||||
GOOS=darwin GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-amd64
|
||||
GOOS=darwin GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-arm64
|
||||
# Windows
|
||||
GOOS=windows GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Multi-platform build complete$(COLOR_RESET)"
|
||||
|
||||
install: build ## Install aptly to GOPATH/bin
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Installing aptly...$(COLOR_RESET)"
|
||||
@cp $(BUILD_DIR)/$(BINARY_NAME) $(BINPATH)/
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Installed to $(BINPATH)/$(BINARY_NAME)$(COLOR_RESET)"
|
||||
|
||||
##@ Testing
|
||||
|
||||
test: prepare test-unit test-integration ## Run all tests
|
||||
|
||||
test-unit: prepare swagger etcd-install ## Run unit tests
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Running unit tests...$(COLOR_RESET)"
|
||||
@mkdir -p $(COVERAGE_DIR)
|
||||
$(GOTEST) -v -race -coverprofile=$(COVERAGE_DIR)/unit.out -covermode=atomic ./...
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Unit tests complete$(COLOR_RESET)"
|
||||
|
||||
test-integration: prepare swagger etcd-install ## Run integration tests
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Running integration tests...$(COLOR_RESET)"
|
||||
@mkdir -p $(COVERAGE_DIR)
|
||||
# Download fixtures if needed
|
||||
@if [ ! -e ~/aptly-fixture-db ]; then \
|
||||
git clone https://github.com/aptly-dev/aptly-fixture-db.git ~/aptly-fixture-db/; \
|
||||
version: ## Print aptly version
|
||||
@ci="" ; \
|
||||
if [ "`make -s releasetype`" = "ci" ]; then \
|
||||
ci=`TZ=UTC git show -s --format='+%cd.%h' --date=format-local:'%Y%m%d%H%M%S'`; \
|
||||
fi ; \
|
||||
if which dpkg-parsechangelog > /dev/null 2>&1; then \
|
||||
echo `dpkg-parsechangelog -S Version`$$ci; \
|
||||
else \
|
||||
echo `grep ^aptly -m1 debian/changelog | sed 's/.*(\([^)]\+\)).*/\1/'`$$ci ; \
|
||||
fi
|
||||
@if [ ! -e ~/aptly-fixture-pool ]; then \
|
||||
git clone https://github.com/aptly-dev/aptly-fixture-pool.git ~/aptly-fixture-pool/; \
|
||||
fi
|
||||
# Run system tests
|
||||
PATH=$(BINPATH):$$PATH python3 system/run.py --coverage-dir $(COVERAGE_DIR) $(TEST)
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Integration tests complete$(COLOR_RESET)"
|
||||
|
||||
test-race: ## Run tests with race detector
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Running tests with race detector...$(COLOR_RESET)"
|
||||
$(GOTEST) -race -short ./...
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Race detection complete$(COLOR_RESET)"
|
||||
swagger-install:
|
||||
# Install swag
|
||||
@test -f $(BINPATH)/swag || GOOS= GOARCH= go install github.com/swaggo/swag/cmd/swag@latest
|
||||
# Generate swagger.conf
|
||||
cp docs/swagger.conf.tpl docs/swagger.conf
|
||||
echo "// @version $(VERSION)" >> docs/swagger.conf
|
||||
|
||||
coverage: test ## Generate coverage report
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Generating coverage report...$(COLOR_RESET)"
|
||||
@mkdir -p $(COVERAGE_DIR)
|
||||
@go tool cover -html=$(COVERAGE_DIR)/unit.out -o $(COVERAGE_DIR)/coverage.html
|
||||
@go tool cover -func=$(COVERAGE_DIR)/unit.out
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Coverage report: $(COVERAGE_DIR)/coverage.html$(COLOR_RESET)"
|
||||
|
||||
benchmark: ## Run benchmarks
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Running benchmarks...$(COLOR_RESET)"
|
||||
$(GOTEST) -bench=. -benchmem ./deb ./files ./utils
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Benchmarks complete$(COLOR_RESET)"
|
||||
|
||||
##@ Code Quality
|
||||
|
||||
lint: dev-tools ## Run linters
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Running linters...$(COLOR_RESET)"
|
||||
@golangci-lint run --timeout=5m
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Linting complete$(COLOR_RESET)"
|
||||
|
||||
fmt: ## Format code
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Formatting code...$(COLOR_RESET)"
|
||||
@$(GOFMT) -w -s .
|
||||
@$(GOMOD) tidy
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Code formatted$(COLOR_RESET)"
|
||||
|
||||
vet: ## Run go vet
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Running go vet...$(COLOR_RESET)"
|
||||
@go vet ./...
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Vet complete$(COLOR_RESET)"
|
||||
|
||||
security: dev-tools ## Run security checks
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Running security checks...$(COLOR_RESET)"
|
||||
@govulncheck ./...
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Security check complete$(COLOR_RESET)"
|
||||
|
||||
##@ Dependencies
|
||||
|
||||
deps-update: ## Update dependencies
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Updating dependencies...$(COLOR_RESET)"
|
||||
@./scripts/update-deps.sh
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Dependencies updated$(COLOR_RESET)"
|
||||
|
||||
deps-check: ## Check for outdated dependencies
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Checking for outdated dependencies...$(COLOR_RESET)"
|
||||
@go list -u -m all | grep '\[' || echo "All dependencies are up to date!"
|
||||
|
||||
deps-graph: ## Generate dependency graph
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Generating dependency graph...$(COLOR_RESET)"
|
||||
@go mod graph | grep -v '@' | sort | uniq
|
||||
|
||||
##@ Documentation
|
||||
|
||||
swagger: swagger-install ## Generate Swagger documentation
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Generating Swagger documentation...$(COLOR_RESET)"
|
||||
@cp docs/swagger.conf.tpl docs/swagger.conf
|
||||
@echo "// @version $(VERSION)" >> docs/swagger.conf
|
||||
@swag init --parseDependency --parseInternal --markdownFiles docs --generalInfo docs/swagger.conf
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Swagger docs generated$(COLOR_RESET)"
|
||||
|
||||
swagger-install: ## Install swagger tools
|
||||
@test -f $(BINPATH)/swag || go install github.com/swaggo/swag/cmd/swag@$(SWAG_VERSION)
|
||||
|
||||
docs: swagger ## Generate all documentation
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Documentation generated$(COLOR_RESET)"
|
||||
|
||||
##@ Development Server
|
||||
|
||||
serve: dev-tools prepare ## Run development server with hot reload
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Starting development server...$(COLOR_RESET)"
|
||||
@cp debian/aptly.conf ~/.aptly.conf || true
|
||||
@sed -i.bak '/enable_swagger_endpoint/s/false/true/' ~/.aptly.conf || true
|
||||
@air -build.pre_cmd 'swag init -q --markdownFiles docs --generalInfo docs/swagger.conf' \
|
||||
-build.exclude_dir docs,system,debian,pgp/keyrings,pgp/test-bins,completion.d,man,deb/testdata,console,_man,systemd,obj-x86_64-linux-gnu \
|
||||
-- api serve -listen 0.0.0.0:3142
|
||||
|
||||
##@ Docker
|
||||
|
||||
docker-build: ## Build Docker image
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Building Docker image...$(COLOR_RESET)"
|
||||
docker build -t $(DOCKER_IMAGE):$(DOCKER_TAG) -t $(DOCKER_IMAGE):latest .
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Docker image built: $(DOCKER_IMAGE):$(DOCKER_TAG)$(COLOR_RESET)"
|
||||
|
||||
docker-push: ## Push Docker image
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Pushing Docker image...$(COLOR_RESET)"
|
||||
docker push $(DOCKER_IMAGE):$(DOCKER_TAG)
|
||||
docker push $(DOCKER_IMAGE):latest
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Docker image pushed$(COLOR_RESET)"
|
||||
|
||||
##@ Cleanup
|
||||
|
||||
clean: ## Clean build artifacts
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Cleaning build artifacts...$(COLOR_RESET)"
|
||||
@rm -rf $(BUILD_DIR) $(COVERAGE_DIR)
|
||||
@rm -f docs/docs.go docs/swagger.json docs/swagger.yaml docs/swagger.conf
|
||||
@rm -rf obj-* *.out *.test
|
||||
@docker-compose -f docker-compose.ci.yml down || true
|
||||
@docker volume prune -f || true
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Clean complete$(COLOR_RESET)"
|
||||
|
||||
clean-deps: ## Clean dependency cache
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Cleaning dependency cache...$(COLOR_RESET)"
|
||||
@go clean -modcache
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Dependency cache cleaned$(COLOR_RESET)"
|
||||
|
||||
##@ CI/CD
|
||||
|
||||
ci: prepare lint test security ## Run CI pipeline
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ CI pipeline complete$(COLOR_RESET)"
|
||||
|
||||
release: clean build-all ## Prepare release artifacts
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Preparing release...$(COLOR_RESET)"
|
||||
@mkdir -p $(BUILD_DIR)/release
|
||||
@for file in $(BUILD_DIR)/$(BINARY_NAME)-*; do \
|
||||
base=$$(basename $$file); \
|
||||
tar -czf $(BUILD_DIR)/release/$$base.tar.gz -C $(BUILD_DIR) $$base; \
|
||||
done
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Release artifacts ready in $(BUILD_DIR)/release$(COLOR_RESET)"
|
||||
|
||||
##@ Utilities
|
||||
|
||||
etcd-install: ## Install etcd for testing
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Starting fresh etcd container...$(COLOR_RESET)"
|
||||
@docker-compose -f docker-compose.ci.yml down etcd 2>/dev/null || true
|
||||
@docker-compose -f docker-compose.ci.yml rm -f -v etcd 2>/dev/null || true
|
||||
@docker volume rm aptly_etcd-data 2>/dev/null || true
|
||||
@docker-compose -f docker-compose.ci.yml up -d etcd
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Waiting for etcd to be ready...$(COLOR_RESET)"
|
||||
@sleep 5
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ etcd started in Docker$(COLOR_RESET)"
|
||||
|
||||
etcd-start: ## Start etcd
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Starting fresh etcd container...$(COLOR_RESET)"
|
||||
@docker-compose -f docker-compose.ci.yml down etcd 2>/dev/null || true
|
||||
@docker-compose -f docker-compose.ci.yml rm -f -v etcd 2>/dev/null || true
|
||||
@docker volume rm aptly_etcd-data 2>/dev/null || true
|
||||
@docker-compose -f docker-compose.ci.yml up -d etcd
|
||||
@sleep 5
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ etcd started in Docker$(COLOR_RESET)"
|
||||
|
||||
etcd-stop: ## Stop etcd
|
||||
@docker-compose -f docker-compose.ci.yml down etcd 2>/dev/null || true
|
||||
@docker-compose -f docker-compose.ci.yml rm -f -v etcd 2>/dev/null || true
|
||||
@docker volume rm aptly_etcd-data 2>/dev/null || true
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ etcd stopped and cleaned$(COLOR_RESET)"
|
||||
|
||||
azurite-start: ## Start Azurite (Azure Storage Emulator) for tests
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Starting Azurite...$(COLOR_RESET)"
|
||||
@azurite -l /tmp/aptly-azurite > ~/.azurite.log 2>&1 & \
|
||||
azurite-start:
|
||||
azurite -l /tmp/aptly-azurite & \
|
||||
echo $$! > ~/.azurite.pid
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Azurite started (PID: $$(cat ~/.azurite.pid))$(COLOR_RESET)"
|
||||
|
||||
azurite-stop: ## Stop Azurite
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Stopping Azurite...$(COLOR_RESET)"
|
||||
@-kill `cat ~/.azurite.pid` 2>/dev/null || true
|
||||
@rm -f ~/.azurite.pid
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Azurite stopped$(COLOR_RESET)"
|
||||
azurite-stop:
|
||||
@kill `cat ~/.azurite.pid`
|
||||
|
||||
.PHONY: all build build-all install test test-unit test-integration test-race coverage benchmark \
|
||||
lint fmt vet security deps-update deps-check deps-graph docs swagger swagger-install serve \
|
||||
docker-build docker-push clean clean-deps ci release prepare dev-tools etcd-install etcd-start etcd-stop \
|
||||
azurite-start azurite-stop
|
||||
swagger: swagger-install
|
||||
# Generate swagger docs
|
||||
@PATH=$(BINPATH)/:$(PATH) swag init --parseDependency --parseInternal --markdownFiles docs --generalInfo docs/swagger.conf
|
||||
|
||||
etcd-install:
|
||||
# Install etcd
|
||||
test -d /tmp/aptly-etcd || system/t13_etcd/install-etcd.sh
|
||||
|
||||
flake8: ## run flake8 on system test python files
|
||||
flake8 system/
|
||||
|
||||
lint: prepare
|
||||
# Install golangci-lint
|
||||
@test -f $(BINPATH)/golangci-lint || go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION)
|
||||
# Running lint
|
||||
@PATH=$(BINPATH)/:$(PATH) golangci-lint run
|
||||
|
||||
|
||||
build: prepare swagger ## Build aptly
|
||||
go build -o build/aptly
|
||||
|
||||
install:
|
||||
@echo "\e[33m\e[1mBuilding aptly ...\e[0m"
|
||||
# go generate
|
||||
@go generate
|
||||
# go install -v
|
||||
@out=`mktemp`; if ! go install -v > $$out 2>&1; then cat $$out; rm -f $$out; echo "\nBuild failed\n"; exit 1; else rm -f $$out; fi
|
||||
|
||||
test: prepare swagger etcd-install ## Run unit tests
|
||||
@echo "\e[33m\e[1mStarting etcd ...\e[0m"
|
||||
@mkdir -p /tmp/aptly-etcd-data; system/t13_etcd/start-etcd.sh > /tmp/aptly-etcd-data/etcd.log 2>&1 &
|
||||
@echo "\e[33m\e[1mRunning go test ...\e[0m"
|
||||
go test -v ./... -gocheck.v=true -coverprofile=unit.out; echo $$? > .unit-test.ret
|
||||
@echo "\e[33m\e[1mStopping etcd ...\e[0m"
|
||||
@pid=`cat /tmp/etcd.pid`; kill $$pid
|
||||
@rm -f /tmp/aptly-etcd-data/etcd.log
|
||||
@ret=`cat .unit-test.ret`; if [ "$$ret" = "0" ]; then echo "\n\e[32m\e[1mUnit Tests SUCCESSFUL\e[0m"; else echo "\n\e[31m\e[1mUnit Tests FAILED\e[0m"; fi; rm -f .unit-test.ret; exit $$ret
|
||||
|
||||
system-test: prepare swagger etcd-install ## Run system tests
|
||||
# build coverage binary
|
||||
go test -v -coverpkg="./..." -c -tags testruncli
|
||||
# Download fixture-db, fixture-pool, etcd.db
|
||||
if [ ! -e ~/aptly-fixture-db ]; then git clone https://github.com/aptly-dev/aptly-fixture-db.git ~/aptly-fixture-db/; fi
|
||||
if [ ! -e ~/aptly-fixture-pool ]; then git clone https://github.com/aptly-dev/aptly-fixture-pool.git ~/aptly-fixture-pool/; fi
|
||||
test -f ~/etcd.db || (curl -o ~/etcd.db.xz http://repo.aptly.info/system-tests/etcd.db.xz && xz -d ~/etcd.db.xz)
|
||||
# Run system tests
|
||||
PATH=$(BINPATH)/:$(PATH) && FORCE_COLOR=1 $(PYTHON) system/run.py --long --coverage-dir $(COVERAGE_DIR) $(CAPTURE) $(TEST)
|
||||
|
||||
bench:
|
||||
@echo "\e[33m\e[1mRunning benchmark ...\e[0m"
|
||||
go test -v ./deb -run=nothing -bench=. -benchmem
|
||||
|
||||
serve: prepare swagger-install ## Run development server (auto recompiling)
|
||||
test -f $(BINPATH)/air || go install github.com/air-verse/air@v1.52.3
|
||||
cp debian/aptly.conf ~/.aptly.conf
|
||||
sed -i /enable_swagger_endpoint/s/false/true/ ~/.aptly.conf
|
||||
PATH=$(BINPATH):$$PATH air -build.pre_cmd 'swag init -q --markdownFiles docs --generalInfo docs/swagger.conf' -build.exclude_dir docs,system,debian,pgp/keyrings,pgp/test-bins,completion.d,man,deb/testdata,console,_man,systemd,obj-x86_64-linux-gnu -- api serve -listen 0.0.0.0:3142
|
||||
|
||||
dpkg: prepare swagger ## Build debian packages
|
||||
@test -n "$(DEBARCH)" || (echo "please define DEBARCH"; exit 1)
|
||||
# set debian version
|
||||
@if [ "`make -s releasetype`" = "ci" ]; then \
|
||||
echo CI Build, setting version... ; \
|
||||
test ! -f debian/changelog.dpkg-bak || mv debian/changelog.dpkg-bak debian/changelog ; \
|
||||
cp debian/changelog debian/changelog.dpkg-bak ; \
|
||||
DEBEMAIL="CI <ci@aptly.info>" dch -v `make -s version` "CI build" ; \
|
||||
fi
|
||||
# clean
|
||||
rm -rf obj-i686-linux-gnu obj-arm-linux-gnueabihf obj-aarch64-linux-gnu obj-x86_64-linux-gnu
|
||||
# Run dpkg-buildpackage
|
||||
@buildtype="any" ; \
|
||||
if [ "$(DEBARCH)" = "amd64" ]; then \
|
||||
buildtype="any,all" ; \
|
||||
fi ; \
|
||||
echo "\e[33m\e[1mBuilding: $$buildtype\e[0m" ; \
|
||||
cmd="dpkg-buildpackage -us -uc --build=$$buildtype -d --host-arch=$(DEBARCH)" ; \
|
||||
echo "$$cmd" ; \
|
||||
$$cmd
|
||||
lintian ../*_$(DEBARCH).changes || true
|
||||
# cleanup
|
||||
@test ! -f debian/changelog.dpkg-bak || mv debian/changelog.dpkg-bak debian/changelog; \
|
||||
mkdir -p build && mv ../*.deb build/ ; \
|
||||
cd build && ls -l *.deb
|
||||
|
||||
binaries: prepare swagger ## Build binary releases (FreeBSD, macOS, Linux generic)
|
||||
# build aptly
|
||||
GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o build/tmp/aptly -ldflags='-extldflags=-static'
|
||||
# install
|
||||
@mkdir -p build/tmp/man build/tmp/completion/bash_completion.d build/tmp/completion/zsh/vendor-completions
|
||||
@cp man/aptly.1 build/tmp/man/
|
||||
@cp completion.d/aptly build/tmp/completion/bash_completion.d/
|
||||
@cp completion.d/_aptly build/tmp/completion/zsh/vendor-completions/
|
||||
@cp README.rst LICENSE AUTHORS build/tmp/
|
||||
@gzip -f build/tmp/man/aptly.1
|
||||
@path="aptly_$(VERSION)_$(GOOS)_$(GOARCH)"; \
|
||||
rm -rf "build/$$path"; \
|
||||
mv build/tmp build/"$$path"; \
|
||||
rm -rf build/tmp; \
|
||||
cd build; \
|
||||
zip -r "$$path".zip "$$path" > /dev/null \
|
||||
&& echo "Built build/$${path}.zip"; \
|
||||
rm -rf "$$path"
|
||||
|
||||
docker-image: ## Build aptly-dev docker image
|
||||
@docker build -f system/Dockerfile . -t aptly-dev
|
||||
|
||||
docker-build: ## Build aptly in docker container
|
||||
@docker run -it --rm -v ${PWD}:/work/src aptly-dev /work/src/system/docker-wrapper build
|
||||
|
||||
docker-shell: ## Run aptly and other commands in docker container
|
||||
@docker run -it --rm -p 3142:3142 -v ${PWD}:/work/src aptly-dev /work/src/system/docker-wrapper || true
|
||||
|
||||
docker-deb: ## Build debian packages in docker container
|
||||
@docker run -it --rm -v ${PWD}:/work/src aptly-dev /work/src/system/docker-wrapper dpkg DEBARCH=amd64
|
||||
|
||||
docker-unit-test: ## Run unit tests in docker container
|
||||
@docker run -it --rm -v ${PWD}:/work/src aptly-dev /work/src/system/docker-wrapper \
|
||||
azurite-start \
|
||||
AZURE_STORAGE_ENDPOINT=http://127.0.0.1:10000/devstoreaccount1 \
|
||||
AZURE_STORAGE_ACCOUNT=devstoreaccount1 \
|
||||
AZURE_STORAGE_ACCESS_KEY="Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==" \
|
||||
test \
|
||||
azurite-stop
|
||||
|
||||
docker-system-test: ## Run system tests in docker container (add TEST=t04_mirror or TEST=UpdateMirror26Test to run only specific tests)
|
||||
@docker run -it --rm -v ${PWD}:/work/src aptly-dev /work/src/system/docker-wrapper \
|
||||
azurite-start \
|
||||
AZURE_STORAGE_ENDPOINT=http://127.0.0.1:10000/devstoreaccount1 \
|
||||
AZURE_STORAGE_ACCOUNT=devstoreaccount1 \
|
||||
AZURE_STORAGE_ACCESS_KEY="Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==" \
|
||||
system-test TEST=$(TEST) \
|
||||
azurite-stop
|
||||
|
||||
docker-serve: ## Run development server (auto recompiling) on http://localhost:3142
|
||||
@docker run -it --rm -p 3142:3142 -v ${PWD}:/work/src aptly-dev /work/src/system/docker-wrapper serve || true
|
||||
|
||||
docker-lint: ## Run golangci-lint in docker container
|
||||
@docker run -it --rm -v ${PWD}:/work/src aptly-dev /work/src/system/docker-wrapper lint
|
||||
|
||||
docker-binaries: ## Build binary releases (FreeBSD, macOS, Linux generic) in docker container
|
||||
@docker run -it --rm -v ${PWD}:/work/src aptly-dev /work/src/system/docker-wrapper binaries
|
||||
|
||||
docker-man: ## Create man page in docker container
|
||||
@docker run -it --rm -v ${PWD}:/work/src aptly-dev /work/src/system/docker-wrapper man
|
||||
|
||||
mem.png: mem.dat mem.gp
|
||||
gnuplot mem.gp
|
||||
open mem.png
|
||||
|
||||
man: ## Create man pages
|
||||
make -C man
|
||||
|
||||
clean: ## remove local build and module cache
|
||||
# Clean all generated and build files
|
||||
test ! -e .go || find .go/ -type d ! -perm -u=w -exec chmod u+w {} \;
|
||||
rm -rf .go/
|
||||
rm -rf build/ obj-*-linux-gnu* tmp/
|
||||
rm -f unit.out aptly.test VERSION docs/docs.go docs/swagger.json docs/swagger.yaml docs/swagger.conf
|
||||
find system/ -type d -name __pycache__ -exec rm -rf {} \; 2>/dev/null || true
|
||||
|
||||
.PHONY: help man prepare swagger version binaries build docker-release docker-system-test docker-unit-test docker-lint docker-build docker-image docker-man docker-shell docker-serve clean releasetype dpkg serve flake8
|
||||
|
||||
-95
@@ -135,98 +135,3 @@ Scala sbt:
|
||||
Molior:
|
||||
|
||||
- `Molior Debian Build System <https://github.com/molior-dbs/molior>`_ by André Roth
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
etcd Database Configuration
|
||||
---------------------------
|
||||
|
||||
When using etcd as the database backend, aptly supports several environment variables for configuration:
|
||||
|
||||
**Timeout Configuration:**
|
||||
|
||||
- ``APTLY_ETCD_TIMEOUT``: Operation timeout for etcd requests (default: ``60s``)
|
||||
|
||||
Example: ``export APTLY_ETCD_TIMEOUT=30s``
|
||||
|
||||
- ``APTLY_ETCD_DIAL_TIMEOUT``: Connection timeout when establishing etcd connection (default: ``60s``)
|
||||
|
||||
Example: ``export APTLY_ETCD_DIAL_TIMEOUT=10s``
|
||||
|
||||
**Connection Configuration:**
|
||||
|
||||
- ``APTLY_ETCD_KEEPALIVE``: Keep-alive timeout for etcd connections (default: ``7200s``)
|
||||
|
||||
Example: ``export APTLY_ETCD_KEEPALIVE=3600s``
|
||||
|
||||
- ``APTLY_ETCD_MAX_MSG_SIZE``: Maximum message size in bytes for etcd requests/responses (default: ``52428800`` - 50MB)
|
||||
|
||||
Example: ``export APTLY_ETCD_MAX_MSG_SIZE=104857600`` # 100MB
|
||||
|
||||
**Example Configuration:**
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Set shorter timeouts for faster failure detection
|
||||
export APTLY_ETCD_TIMEOUT=30s
|
||||
export APTLY_ETCD_DIAL_TIMEOUT=10s
|
||||
|
||||
# Increase message size for large package operations
|
||||
export APTLY_ETCD_MAX_MSG_SIZE=104857600
|
||||
|
||||
# Run aptly with etcd backend
|
||||
aptly -config=/etc/aptly-etcd.conf mirror update debian-stable
|
||||
|
||||
**Features:**
|
||||
|
||||
- **Automatic Retry**: Read operations (Get) automatically retry up to 3 times with exponential backoff on temporary failures
|
||||
- **Timeout Protection**: All etcd operations use context with timeout to prevent indefinite hangs
|
||||
- **Enhanced Logging**: All etcd errors are logged with operation context for better debugging
|
||||
- **Configurable Limits**: Message size limits can be adjusted for large package operations
|
||||
|
||||
etcd Write Queue Configuration
|
||||
------------------------------
|
||||
|
||||
To prevent etcd overload during concurrent operations (e.g., multiple mirror updates), aptly supports an optional write queue that serializes database write operations:
|
||||
|
||||
**Configuration in aptly.conf:**
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"databaseBackend": {
|
||||
"type": "etcd",
|
||||
"url": "localhost:2379",
|
||||
"timeout": "120s",
|
||||
"writeRetries": 3,
|
||||
"writeQueue": {
|
||||
"enabled": true,
|
||||
"queueSize": 1000,
|
||||
"maxWritesPerSec": 100,
|
||||
"batchMaxSize": 50,
|
||||
"batchMaxWaitMs": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
**Write Queue Options:**
|
||||
|
||||
- ``enabled``: Enable/disable the write queue (default: ``false``)
|
||||
- ``queueSize``: Size of the write operation queue (default: ``1000``)
|
||||
- ``maxWritesPerSec``: Maximum write operations per second (default: ``100``)
|
||||
- ``batchMaxSize``: Maximum batch size for future batching support (default: ``50``)
|
||||
- ``batchMaxWaitMs``: Maximum wait time for batch accumulation in milliseconds (default: ``10``)
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- **Prevents etcd Overload**: Serializes write operations to avoid overwhelming etcd
|
||||
- **Maintains Parallelism**: I/O operations like downloads remain parallel
|
||||
- **Rate Limiting**: Configurable writes per second to match etcd capacity
|
||||
- **Transparent**: No code changes required, just enable in configuration
|
||||
|
||||
**Example Impact:**
|
||||
|
||||
Without write queue: 5 mirror updates → 5 parallel writers → 1000s of concurrent etcd operations → timeouts
|
||||
|
||||
With write queue: 5 mirror updates → 5 parallel processes → 1 sequential etcd writer → stable performance
|
||||
|
||||
+1
-2
@@ -12,6 +12,5 @@ git push origin v$version master
|
||||
```
|
||||
- run swagger locally (`make docker-serve`)
|
||||
- copy generated docs/swagger.json to https://github.com/aptly-dev/www.aptly.info/tree/master/static/swagger/aptly_1.x.y.json
|
||||
- add new version to select tag in content/doc/api/swagger.md line 48
|
||||
- push commit to master
|
||||
- releae www.aptly.info
|
||||
- create release announcement on https://github.com/aptly-dev/aptly/discussions
|
||||
|
||||
+24
-38
@@ -7,7 +7,6 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
@@ -42,10 +41,7 @@ type aptlyVersion struct {
|
||||
// @Success 200 {object} aptlyVersion
|
||||
// @Router /api/version [get]
|
||||
func apiVersion(c *gin.Context) {
|
||||
version := aptlyVersion{
|
||||
Version: aptly.Version,
|
||||
}
|
||||
c.JSON(200, version)
|
||||
c.JSON(200, gin.H{"Version": aptly.Version})
|
||||
}
|
||||
|
||||
type aptlyStatus struct {
|
||||
@@ -71,8 +67,7 @@ func apiReady(isReady *atomic.Value) func(*gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
status := aptlyStatus{Status: "Aptly is ready"}
|
||||
c.JSON(200, status)
|
||||
c.JSON(200, gin.H{"Status": "Aptly is ready"})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,18 +96,7 @@ type dbRequest struct {
|
||||
err chan<- error
|
||||
}
|
||||
|
||||
var (
|
||||
dbRequests chan dbRequest
|
||||
dbRequestsOnce sync.Once
|
||||
)
|
||||
|
||||
// initDBRequests initializes the database request channel in a thread-safe manner
|
||||
func initDBRequests() {
|
||||
dbRequestsOnce.Do(func() {
|
||||
dbRequests = make(chan dbRequest, 1)
|
||||
go acquireDatabase()
|
||||
})
|
||||
}
|
||||
var dbRequests chan dbRequest
|
||||
|
||||
// Acquire database lock and release it when not needed anymore.
|
||||
//
|
||||
@@ -151,8 +135,9 @@ func acquireDatabase() {
|
||||
// runTaskInBackground to run a task which accquire database.
|
||||
// Important do not forget to defer to releaseDatabaseConnection
|
||||
func acquireDatabaseConnection() error {
|
||||
// Ensure channel is initialized
|
||||
initDBRequests()
|
||||
if dbRequests == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
errCh := make(chan error)
|
||||
dbRequests <- dbRequest{acquiredb, errCh}
|
||||
@@ -162,8 +147,9 @@ func acquireDatabaseConnection() error {
|
||||
|
||||
// Release database connection when not needed anymore
|
||||
func releaseDatabaseConnection() error {
|
||||
// Ensure channel is initialized
|
||||
initDBRequests()
|
||||
if dbRequests == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
errCh := make(chan error)
|
||||
dbRequests <- dbRequest{releasedb, errCh}
|
||||
@@ -179,7 +165,7 @@ func runTaskInBackground(name string, resources []string, proc task.Process) (ta
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() { _ = releaseDatabaseConnection() }()
|
||||
defer releaseDatabaseConnection()
|
||||
return proc(out, detail)
|
||||
})
|
||||
}
|
||||
@@ -188,18 +174,18 @@ func truthy(value interface{}) bool {
|
||||
if value == nil {
|
||||
return false
|
||||
}
|
||||
switch v := value.(type) {
|
||||
switch value.(type) {
|
||||
case string:
|
||||
switch strings.ToLower(v) {
|
||||
switch strings.ToLower(value.(string)) {
|
||||
case "n", "no", "f", "false", "0", "off":
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
case int:
|
||||
return v != 0
|
||||
return !(value.(int) == 0)
|
||||
case bool:
|
||||
return v
|
||||
return value.(bool)
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -224,11 +210,11 @@ func maybeRunTaskInBackground(c *gin.Context, name string, resources []string, p
|
||||
}
|
||||
|
||||
// wait for task to finish
|
||||
_, _ = context.TaskList().WaitForTaskByID(task.ID)
|
||||
context.TaskList().WaitForTaskByID(task.ID)
|
||||
|
||||
retValue, _ := context.TaskList().GetTaskReturnValueByID(task.ID)
|
||||
err, _ := context.TaskList().GetTaskErrorByID(task.ID)
|
||||
_, _ = context.TaskList().DeleteTaskByID(task.ID)
|
||||
context.TaskList().DeleteTaskByID(task.ID)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, retValue.Code, err)
|
||||
return
|
||||
@@ -296,11 +282,11 @@ func showPackages(c *gin.Context, reflist *deb.PackageRefList, collectionFactory
|
||||
// filter packages by version
|
||||
if c.Request.URL.Query().Get("maximumVersion") == "1" {
|
||||
list.PrepareIndex()
|
||||
_ = list.ForEach(func(p *deb.Package) error {
|
||||
list.ForEach(func(p *deb.Package) error {
|
||||
versionQ, err := query.Parse(fmt.Sprintf("Name (%s), $Version (<= %s)", p.Name, p.Version))
|
||||
if err != nil {
|
||||
fmt.Println("filter packages by version, query string parse err: ", err)
|
||||
_ = c.AbortWithError(500, fmt.Errorf("unable to parse %s maximum version query string: %s", p.Name, err))
|
||||
c.AbortWithError(500, fmt.Errorf("unable to parse %s maximum version query string: %s", p.Name, err))
|
||||
} else {
|
||||
tmpList, err := list.Filter(deb.FilterOptions{
|
||||
Queries: []deb.PackageQuery{versionQ},
|
||||
@@ -308,15 +294,15 @@ func showPackages(c *gin.Context, reflist *deb.PackageRefList, collectionFactory
|
||||
|
||||
if err == nil {
|
||||
if tmpList.Len() > 0 {
|
||||
_ = tmpList.ForEach(func(tp *deb.Package) error {
|
||||
tmpList.ForEach(func(tp *deb.Package) error {
|
||||
list.Remove(tp)
|
||||
return nil
|
||||
})
|
||||
_ = list.Add(p)
|
||||
list.Add(p)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("filter packages by version, filter err: ", err)
|
||||
_ = c.AbortWithError(500, fmt.Errorf("unable to get %s maximum version: %s", p.Name, err))
|
||||
c.AbortWithError(500, fmt.Errorf("unable to get %s maximum version: %s", p.Name, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,7 +311,7 @@ func showPackages(c *gin.Context, reflist *deb.PackageRefList, collectionFactory
|
||||
}
|
||||
|
||||
if c.Request.URL.Query().Get("format") == "details" {
|
||||
_ = list.ForEach(func(p *deb.Package) error {
|
||||
list.ForEach(func(p *deb.Package) error {
|
||||
result = append(result, p)
|
||||
return nil
|
||||
})
|
||||
@@ -336,7 +322,7 @@ func showPackages(c *gin.Context, reflist *deb.PackageRefList, collectionFactory
|
||||
}
|
||||
}
|
||||
|
||||
func AbortWithJSONError(c *gin.Context, code int, err error) {
|
||||
func AbortWithJSONError(c *gin.Context, code int, err error) *gin.Error {
|
||||
c.Writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
_ = c.AbortWithError(code, err)
|
||||
return c.AbortWithError(code, err)
|
||||
}
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/gin-gonic/gin"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type ApiPackagesSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&ApiPackagesSuite{})
|
||||
|
||||
func (s *ApiPackagesSuite) TestShowPackages(c *C) {
|
||||
// Test showPackages function with nil reflist
|
||||
w := httptest.NewRecorder()
|
||||
ginCtx, _ := gin.CreateTestContext(w)
|
||||
ginCtx.Request = httptest.NewRequest("GET", "/api/test", nil)
|
||||
|
||||
showPackages(ginCtx, nil, s.context.NewCollectionFactory())
|
||||
|
||||
// Should return 404 for nil reflist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *ApiPackagesSuite) TestShowPackagesWithEmptyList(c *C) {
|
||||
// Test showPackages with empty package reflist
|
||||
w := httptest.NewRecorder()
|
||||
ginCtx, _ := gin.CreateTestContext(w)
|
||||
ginCtx.Request = httptest.NewRequest("GET", "/api/test", nil)
|
||||
|
||||
reflist := deb.NewPackageRefList()
|
||||
showPackages(ginCtx, reflist, s.context.NewCollectionFactory())
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
|
||||
var result []string
|
||||
err := json.Unmarshal(w.Body.Bytes(), &result)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(len(result), Equals, 0)
|
||||
}
|
||||
|
||||
func (s *ApiPackagesSuite) TestShowPackagesCompact(c *C) {
|
||||
// Test showPackages with compact format (default)
|
||||
w := httptest.NewRecorder()
|
||||
ginCtx, _ := gin.CreateTestContext(w)
|
||||
ginCtx.Request = httptest.NewRequest("GET", "/api/test", nil)
|
||||
|
||||
reflist := deb.NewPackageRefList()
|
||||
showPackages(ginCtx, reflist, s.context.NewCollectionFactory())
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
}
|
||||
|
||||
func (s *ApiPackagesSuite) TestShowPackagesDetails(c *C) {
|
||||
// Test showPackages with details format
|
||||
w := httptest.NewRecorder()
|
||||
ginCtx, _ := gin.CreateTestContext(w)
|
||||
ginCtx.Request = httptest.NewRequest("GET", "/api/test?format=details", nil)
|
||||
|
||||
reflist := deb.NewPackageRefList()
|
||||
showPackages(ginCtx, reflist, s.context.NewCollectionFactory())
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
|
||||
var result []*deb.Package
|
||||
err := json.Unmarshal(w.Body.Bytes(), &result)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(len(result), Equals, 0)
|
||||
}
|
||||
+18
-201
@@ -13,8 +13,6 @@ import (
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
ctx "github.com/aptly-dev/aptly/context"
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/aptly-dev/aptly/task"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/smira/flag"
|
||||
@@ -26,14 +24,14 @@ func Test(t *testing.T) {
|
||||
TestingT(t)
|
||||
}
|
||||
|
||||
type APISuite struct {
|
||||
type ApiSuite struct {
|
||||
context *ctx.AptlyContext
|
||||
flags *flag.FlagSet
|
||||
configFile *os.File
|
||||
router http.Handler
|
||||
}
|
||||
|
||||
var _ = Suite(&APISuite{})
|
||||
var _ = Suite(&ApiSuite{})
|
||||
|
||||
func createTestConfig() *os.File {
|
||||
file, err := os.CreateTemp("", "aptly")
|
||||
@@ -47,11 +45,11 @@ func createTestConfig() *os.File {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
_, _ = file.Write(jsonString)
|
||||
file.Write(jsonString)
|
||||
return file
|
||||
}
|
||||
|
||||
func (s *APISuite) setupContext() error {
|
||||
func (s *ApiSuite) setupContext() error {
|
||||
aptly.Version = "testVersion"
|
||||
file := createTestConfig()
|
||||
if nil == file {
|
||||
@@ -77,23 +75,23 @@ func (s *APISuite) setupContext() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *APISuite) SetUpSuite(c *C) {
|
||||
func (s *ApiSuite) SetUpSuite(c *C) {
|
||||
err := s.setupContext()
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *APISuite) TearDownSuite(c *C) {
|
||||
_ = os.Remove(s.configFile.Name())
|
||||
func (s *ApiSuite) TearDownSuite(c *C) {
|
||||
os.Remove(s.configFile.Name())
|
||||
s.context.Shutdown()
|
||||
}
|
||||
|
||||
func (s *APISuite) SetUpTest(c *C) {
|
||||
func (s *ApiSuite) SetUpTest(c *C) {
|
||||
}
|
||||
|
||||
func (s *APISuite) TearDownTest(c *C) {
|
||||
func (s *ApiSuite) TearDownTest(c *C) {
|
||||
}
|
||||
|
||||
func (s *APISuite) HTTPRequest(method string, url string, body io.Reader) (*httptest.ResponseRecorder, error) {
|
||||
func (s *ApiSuite) HTTPRequest(method string, url string, body io.Reader) (*httptest.ResponseRecorder, error) {
|
||||
w := httptest.NewRecorder()
|
||||
req, err := http.NewRequest(method, url, body)
|
||||
if err != nil {
|
||||
@@ -104,32 +102,32 @@ func (s *APISuite) HTTPRequest(method string, url string, body io.Reader) (*http
|
||||
return w, nil
|
||||
}
|
||||
|
||||
func (s *APISuite) TestGinRunsInReleaseMode(c *C) {
|
||||
func (s *ApiSuite) TestGinRunsInReleaseMode(c *C) {
|
||||
c.Check(gin.Mode(), Equals, gin.ReleaseMode)
|
||||
}
|
||||
|
||||
func (s *APISuite) TestGetVersion(c *C) {
|
||||
func (s *ApiSuite) TestGetVersion(c *C) {
|
||||
response, err := s.HTTPRequest("GET", "/api/version", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
c.Check(response.Body.String(), Matches, "{\"Version\":\""+aptly.Version+"\"}")
|
||||
}
|
||||
|
||||
func (s *APISuite) TestGetReadiness(c *C) {
|
||||
func (s *ApiSuite) TestGetReadiness(c *C) {
|
||||
response, err := s.HTTPRequest("GET", "/api/ready", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
c.Check(response.Body.String(), Matches, "{\"Status\":\"Aptly is ready\"}")
|
||||
}
|
||||
|
||||
func (s *APISuite) TestGetHealthiness(c *C) {
|
||||
func (s *ApiSuite) TestGetHealthiness(c *C) {
|
||||
response, err := s.HTTPRequest("GET", "/api/healthy", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
c.Check(response.Body.String(), Matches, "{\"Status\":\"Aptly is healthy\"}")
|
||||
}
|
||||
|
||||
func (s *APISuite) TestGetMetrics(c *C) {
|
||||
func (s *ApiSuite) TestGetMetrics(c *C) {
|
||||
response, err := s.HTTPRequest("GET", "/api/metrics", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
@@ -143,22 +141,16 @@ func (s *APISuite) TestGetMetrics(c *C) {
|
||||
c.Check(b, Matches, ".*aptly_build_info.*version=\"testVersion\".*")
|
||||
}
|
||||
|
||||
func (s *APISuite) TestRepoCreate(c *C) {
|
||||
func (s *ApiSuite) TestRepoCreate(c *C) {
|
||||
body, err := json.Marshal(gin.H{
|
||||
"Name": "dummy",
|
||||
})
|
||||
c.Assert(err, IsNil)
|
||||
resp, err := s.HTTPRequest("POST", "/api/repos", bytes.NewReader(body))
|
||||
_, err = s.HTTPRequest("POST", "/api/repos", bytes.NewReader(body))
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(resp.Code, Equals, 201)
|
||||
|
||||
// Clean up: delete the created repo
|
||||
resp, err = s.HTTPRequest("DELETE", "/api/repos/dummy?force=1", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(resp.Code, Equals, 200)
|
||||
}
|
||||
|
||||
func (s *APISuite) TestTruthy(c *C) {
|
||||
func (s *ApiSuite) TestTruthy(c *C) {
|
||||
c.Check(truthy("no"), Equals, false)
|
||||
c.Check(truthy("n"), Equals, false)
|
||||
c.Check(truthy("off"), Equals, false)
|
||||
@@ -181,178 +173,3 @@ func (s *APISuite) TestTruthy(c *C) {
|
||||
c.Check(truthy(-1), Equals, true)
|
||||
c.Check(truthy(gin.H{}), Equals, true)
|
||||
}
|
||||
|
||||
func (s *APISuite) TestDatabaseConnectionFunctions(c *C) {
|
||||
// Test acquire and release database connection
|
||||
err := acquireDatabaseConnection()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
err = releaseDatabaseConnection()
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *APISuite) TestConcurrentDatabaseRequests(c *C) {
|
||||
// Test concurrent database acquisition
|
||||
done := make(chan bool, 5)
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
go func() {
|
||||
defer func() { done <- true }()
|
||||
|
||||
err := acquireDatabaseConnection()
|
||||
if err == nil {
|
||||
_ = releaseDatabaseConnection()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Wait for all goroutines
|
||||
for i := 0; i < 5; i++ {
|
||||
<-done
|
||||
}
|
||||
|
||||
c.Check(true, Equals, true) // If we get here, no deadlock occurred
|
||||
}
|
||||
|
||||
func (s *APISuite) TestMaybeRunTaskInBackground(c *C) {
|
||||
// Test synchronous task execution
|
||||
w := httptest.NewRecorder()
|
||||
ginCtx, _ := gin.CreateTestContext(w)
|
||||
ginCtx.Request = httptest.NewRequest("GET", "/api/test", nil)
|
||||
|
||||
called := false
|
||||
maybeRunTaskInBackground(ginCtx, "test-task", []string{}, func(out aptly.Progress, detail *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
called = true
|
||||
return &task.ProcessReturnValue{Code: 200, Value: gin.H{"status": "ok"}}, nil
|
||||
})
|
||||
|
||||
c.Check(called, Equals, true)
|
||||
c.Check(w.Code, Equals, 200)
|
||||
}
|
||||
|
||||
func (s *APISuite) TestMaybeRunTaskInBackgroundAsync(c *C) {
|
||||
// Test asynchronous task execution
|
||||
w := httptest.NewRecorder()
|
||||
ginCtx, _ := gin.CreateTestContext(w)
|
||||
ginCtx.Request = httptest.NewRequest("GET", "/api/test?_async=true", nil)
|
||||
|
||||
maybeRunTaskInBackground(ginCtx, "test-async-task", []string{}, func(out aptly.Progress, detail *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
return &task.ProcessReturnValue{Code: 200, Value: gin.H{"status": "ok"}}, nil
|
||||
})
|
||||
|
||||
// For async, should return 202 Accepted
|
||||
c.Check(w.Code, Equals, 202)
|
||||
}
|
||||
|
||||
func (s *APISuite) TestAbortWithJSONError(c *C) {
|
||||
w := httptest.NewRecorder()
|
||||
ginCtx, _ := gin.CreateTestContext(w)
|
||||
|
||||
testErr := fmt.Errorf("test error message")
|
||||
AbortWithJSONError(ginCtx, 400, testErr)
|
||||
|
||||
c.Check(w.Code, Equals, 400)
|
||||
c.Check(w.Header().Get("Content-Type"), Equals, "application/json; charset=utf-8")
|
||||
}
|
||||
|
||||
func (s *APISuite) TestShowPackagesWithNilList(c *C) {
|
||||
w := httptest.NewRecorder()
|
||||
ginCtx, _ := gin.CreateTestContext(w)
|
||||
ginCtx.Request = httptest.NewRequest("GET", "/api/test", nil)
|
||||
|
||||
showPackages(ginCtx, nil, s.context.NewCollectionFactory())
|
||||
|
||||
// Should return error when reflist is nil
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *APISuite) TestAPIVersionConstant(c *C) {
|
||||
// Test that apiVersion struct is properly defined
|
||||
version := aptlyVersion{Version: "test-version"}
|
||||
c.Check(version.Version, Equals, "test-version")
|
||||
}
|
||||
|
||||
func (s *APISuite) TestAPIStatusConstant(c *C) {
|
||||
// Test that aptlyStatus struct is properly defined
|
||||
status := aptlyStatus{Status: "test-status"}
|
||||
c.Check(status.Status, Equals, "test-status")
|
||||
}
|
||||
|
||||
func (s *APISuite) TestRunTaskInBackground(c *C) {
|
||||
// Test running task in background
|
||||
task, err := runTaskInBackground("background-test", []string{}, func(out aptly.Progress, detail *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
return &task.ProcessReturnValue{Code: 200, Value: gin.H{"done": true}}, nil
|
||||
})
|
||||
|
||||
c.Check(err, IsNil)
|
||||
c.Check(task, NotNil)
|
||||
c.Check(task.Name, Equals, "background-test")
|
||||
|
||||
// Wait for task to complete
|
||||
_, _ = s.context.TaskList().WaitForTaskByID(task.ID)
|
||||
|
||||
// Clean up
|
||||
_, _ = s.context.TaskList().DeleteTaskByID(task.ID)
|
||||
}
|
||||
|
||||
func (s *APISuite) TestInitDBRequests(c *C) {
|
||||
// Test that initDBRequests can be called multiple times safely
|
||||
initDBRequests()
|
||||
initDBRequests() // Should not panic
|
||||
|
||||
c.Check(dbRequests, NotNil)
|
||||
}
|
||||
|
||||
func (s *APISuite) TestShowPackagesWithQuery(c *C) {
|
||||
// Create a test gin context
|
||||
w := httptest.NewRecorder()
|
||||
ginCtx, _ := gin.CreateTestContext(w)
|
||||
ginCtx.Request = httptest.NewRequest("GET", "/api/test?q=Name&format=details", nil)
|
||||
|
||||
// Create empty reflist
|
||||
reflist := deb.NewPackageRefList()
|
||||
|
||||
showPackages(ginCtx, reflist, s.context.NewCollectionFactory())
|
||||
|
||||
// Should succeed with empty list
|
||||
c.Check(w.Code, Equals, 200)
|
||||
|
||||
var result []*deb.Package
|
||||
err := json.Unmarshal(w.Body.Bytes(), &result)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(len(result), Equals, 0)
|
||||
}
|
||||
|
||||
func (s *APISuite) TestShowPackagesCompactFormat(c *C) {
|
||||
// Test compact format (default)
|
||||
w := httptest.NewRecorder()
|
||||
ginCtx, _ := gin.CreateTestContext(w)
|
||||
ginCtx.Request = httptest.NewRequest("GET", "/api/test", nil)
|
||||
|
||||
reflist := deb.NewPackageRefList()
|
||||
showPackages(ginCtx, reflist, s.context.NewCollectionFactory())
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
|
||||
var result []string
|
||||
err := json.Unmarshal(w.Body.Bytes(), &result)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(len(result), Equals, 0)
|
||||
}
|
||||
|
||||
func (s *APISuite) TestTruthyEdgeCases(c *C) {
|
||||
// Test edge cases for truthy function
|
||||
c.Check(truthy("F"), Equals, false) // capital F
|
||||
c.Check(truthy("FALSE"), Equals, false) // all caps
|
||||
c.Check(truthy("False"), Equals, false) // mixed case
|
||||
c.Check(truthy("NO"), Equals, false) // capital NO
|
||||
c.Check(truthy("Off"), Equals, false) // mixed case off
|
||||
|
||||
// Test empty string
|
||||
c.Check(truthy(""), Equals, true) // empty string is truthy
|
||||
|
||||
// Test other types
|
||||
c.Check(truthy(struct{}{}), Equals, true) // empty struct
|
||||
c.Check(truthy([]int{}), Equals, true) // empty slice
|
||||
c.Check(truthy(map[string]int{}), Equals, true) // empty map
|
||||
}
|
||||
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
func Authorize(username string, password string) (ok bool) {
|
||||
config := context.Config()
|
||||
|
||||
if config.Auth.Type != "" {
|
||||
switch strings.ToLower(config.Auth.Type) {
|
||||
case "ldap":
|
||||
ok = doLdapAuth(username, password)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func doLdapAuth(username string, password string) bool {
|
||||
config := context.Config()
|
||||
attributes := []string{"DN", "CN"}
|
||||
|
||||
server := config.Auth.Server
|
||||
dn := config.Auth.LdapDN
|
||||
filter := fmt.Sprintf(config.Auth.LdapFilter, username)
|
||||
|
||||
// connect to ldap server
|
||||
conn, err := ldap.Dial("tcp", server)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// reconnect via tls
|
||||
err = conn.StartTLS(&tls.Config{InsecureSkipVerify: config.Auth.SecureTLS})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// format our request and then fire it off
|
||||
request := ldap.NewSearchRequest(dn, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, attributes, nil)
|
||||
search, err := conn.Search(request)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
// get our modified dn and then check our user for auth
|
||||
udn := search.Entries[0].DN
|
||||
err = conn.Bind(udn, password)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func getGroups(c *gin.Context, username string) {
|
||||
|
||||
var groups []string
|
||||
config := context.Config()
|
||||
dn := config.Auth.LdapDN
|
||||
session := sessions.Default(c)
|
||||
// connect to ldap server
|
||||
server := fmt.Sprintf("%s", config.Auth.Server)
|
||||
conn, err := ldap.Dial("tcp", server)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// reconnect via tls
|
||||
err = conn.StartTLS(&tls.Config{InsecureSkipVerify: true})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
filter := fmt.Sprintf("(|(member=uid=%s,ou=people,dc=llnw,dc=com)(member=uid=%s,ou=people,dc=llnw,dc=com))", username, username)
|
||||
request := ldap.NewSearchRequest(dn, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, []string{"dn", "cn"}, nil)
|
||||
search, err := conn.Search(request)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(search.Entries) < 1 {
|
||||
return
|
||||
}
|
||||
for _, v := range search.Entries {
|
||||
value := strings.Split(strings.TrimLeft(v.DN, "cn="), ",")[0]
|
||||
groups = append(groups, fmt.Sprintf("%s,", value))
|
||||
}
|
||||
session.Set("Groups", groups)
|
||||
}
|
||||
|
||||
func checkGroup(c *gin.Context, ldgroup string) bool {
|
||||
session := sessions.Default(c)
|
||||
groups := session.Get("Groups")
|
||||
if ldgroup == "" {
|
||||
return true
|
||||
}
|
||||
for _, v := range groups.([]string) {
|
||||
if strings.Contains(v, ldgroup) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func CheckGroup(c *gin.Context, ldgroup string) (err error) {
|
||||
if !checkGroup(c, ldgroup) {
|
||||
err = fmt.Errorf("Authorisation Failred")
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
// @Success 200 {object} string "Output"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Router /api/db/cleanup [post]
|
||||
func apiDBCleanup(c *gin.Context) {
|
||||
func apiDbCleanup(c *gin.Context) {
|
||||
resources := []string{string(task.AllResourcesKey)}
|
||||
maybeRunTaskInBackground(c, "Clean up db", resources, func(out aptly.Progress, detail *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
var err error
|
||||
@@ -109,8 +109,8 @@ func apiDBCleanup(c *gin.Context) {
|
||||
|
||||
if toDelete.Len() > 0 {
|
||||
batch := db.CreateBatch()
|
||||
_ = toDelete.ForEach(func(ref []byte) error {
|
||||
_ = collectionFactory.PackageCollection().DeleteByKey(ref, batch)
|
||||
toDelete.ForEach(func(ref []byte) error {
|
||||
collectionFactory.PackageCollection().DeleteByKey(ref, batch)
|
||||
return nil
|
||||
})
|
||||
|
||||
|
||||
-362
@@ -1,362 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"time"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/task"
|
||||
"github.com/gin-gonic/gin"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type DBTestSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&DBTestSuite{})
|
||||
|
||||
func (s *DBTestSuite) SetUpTest(c *C) {
|
||||
s.APISuite.SetUpTest(c)
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestDbCleanupStructure(c *C) {
|
||||
// Test database cleanup endpoint structure
|
||||
req, _ := http.NewRequest("POST", "/api/db/cleanup", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should succeed with proper context
|
||||
c.Check(w.Code, Equals, 200)
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestDbCleanupWithAsync(c *C) {
|
||||
// Test database cleanup with async parameter
|
||||
req, _ := http.NewRequest("POST", "/api/db/cleanup?_async=1", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should return task response when async
|
||||
c.Check(w.Code, Equals, 202)
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestDbCleanupWithDryRun(c *C) {
|
||||
// Test database cleanup with dry run parameter
|
||||
req, _ := http.NewRequest("POST", "/api/db/cleanup?dry-run=1", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should succeed with dry run
|
||||
c.Check(w.Code, Equals, 200)
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestDbCleanupWithBothParams(c *C) {
|
||||
// Test database cleanup with both async and dry-run parameters
|
||||
req, _ := http.NewRequest("POST", "/api/db/cleanup?_async=1&dry-run=1", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests parameter combination
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestDbCleanupHTTPMethods(c *C) {
|
||||
// Test that only POST method is allowed
|
||||
deniedMethods := []string{"GET", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"}
|
||||
|
||||
for _, method := range deniedMethods {
|
||||
req, _ := http.NewRequest(method, "/api/db/cleanup", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 404, Commentf("Method: %s should be denied", method))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestDbCleanupWithRequestBody(c *C) {
|
||||
// Test database cleanup with various request bodies (should be ignored)
|
||||
testBodies := []string{
|
||||
"",
|
||||
"some random text",
|
||||
`{"key": "value"}`,
|
||||
`<xml>data</xml>`,
|
||||
"binary\x00\x01\x02data",
|
||||
}
|
||||
|
||||
for i, body := range testBodies {
|
||||
req, _ := http.NewRequest("POST", "/api/db/cleanup", bytes.NewBufferString(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle various body content without crashing
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Body test #%d", i+1))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestDbCleanupParameterVariations(c *C) {
|
||||
// Test various parameter value combinations
|
||||
paramTests := []struct {
|
||||
query string
|
||||
description string
|
||||
}{
|
||||
{"", "no parameters"},
|
||||
{"_async=0", "async disabled"},
|
||||
{"_async=false", "async false"},
|
||||
{"_async=true", "async true"},
|
||||
{"dry-run=0", "dry-run disabled"},
|
||||
{"dry-run=false", "dry-run false"},
|
||||
{"dry-run=true", "dry-run true"},
|
||||
{"_async=1&dry-run=0", "async on, dry-run off"},
|
||||
{"_async=0&dry-run=1", "async off, dry-run on"},
|
||||
{"_async=true&dry-run=false", "async true, dry-run false"},
|
||||
{"unknown=param", "unknown parameter"},
|
||||
{"_async=invalid", "invalid async value"},
|
||||
{"dry-run=invalid", "invalid dry-run value"},
|
||||
}
|
||||
|
||||
for _, test := range paramTests {
|
||||
path := "/api/db/cleanup"
|
||||
if test.query != "" {
|
||||
path += "?" + test.query
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest("POST", path, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle all parameter variations without crashing
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Test: %s", test.description))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestDbCleanupContentTypes(c *C) {
|
||||
// Test different content types
|
||||
contentTypes := []string{
|
||||
"",
|
||||
"application/json",
|
||||
"text/plain",
|
||||
"application/x-www-form-urlencoded",
|
||||
"multipart/form-data",
|
||||
"application/octet-stream",
|
||||
}
|
||||
|
||||
for _, contentType := range contentTypes {
|
||||
req, _ := http.NewRequest("POST", "/api/db/cleanup", nil)
|
||||
if contentType != "" {
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle different content types without crashing
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Content-Type: %s", contentType))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestDbCleanupErrorHandling(c *C) {
|
||||
// Test various error conditions
|
||||
errorTests := []struct {
|
||||
description string
|
||||
path string
|
||||
method string
|
||||
expectError bool
|
||||
}{
|
||||
{"Normal cleanup call", "/api/db/cleanup", "POST", true}, // Expect error due to no context
|
||||
{"Cleanup with extra path", "/api/db/cleanup/extra", "POST", false}, // Route not matched
|
||||
{"Cleanup normal path", "/api/db/cleanup", "POST", true}, // Valid endpoint
|
||||
{"Case sensitive path", "/api/DB/cleanup", "POST", false}, // Route not matched
|
||||
{"Case sensitive path", "/api/db/CLEANUP", "POST", false}, // Route not matched
|
||||
}
|
||||
|
||||
for _, test := range errorTests {
|
||||
req, _ := http.NewRequest(test.method, test.path, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// All should return some response without crashing
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Test: %s", test.description))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestDbCleanupReliability(c *C) {
|
||||
// Test multiple sequential calls for reliability
|
||||
for i := 0; i < 5; i++ {
|
||||
req, _ := http.NewRequest("POST", "/api/db/cleanup", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should be consistent across multiple calls
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Call #%d", i+1))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestDbCleanupHeaders(c *C) {
|
||||
// Test with various HTTP headers
|
||||
headerTests := []map[string]string{
|
||||
{},
|
||||
{"Accept": "application/json"},
|
||||
{"Accept": "text/plain"},
|
||||
{"Accept": "*/*"},
|
||||
{"User-Agent": "test-agent"},
|
||||
{"Authorization": "Bearer token123"},
|
||||
{"X-Custom-Header": "custom-value"},
|
||||
{"Accept-Encoding": "gzip, deflate"},
|
||||
{"Accept-Language": "en-US,en;q=0.9"},
|
||||
}
|
||||
|
||||
for i, headers := range headerTests {
|
||||
req, _ := http.NewRequest("POST", "/api/db/cleanup", nil)
|
||||
for key, value := range headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle various headers without crashing
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Header test #%d", i+1))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestDbCleanupResponseFormat(c *C) {
|
||||
// Test response format consistency
|
||||
req, _ := http.NewRequest("POST", "/api/db/cleanup", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should have proper response structure
|
||||
c.Check(w.Code, Not(Equals), 0)
|
||||
c.Check(w.Header(), NotNil)
|
||||
|
||||
// If there's a response body, it should be valid
|
||||
if w.Body.Len() > 0 {
|
||||
body := w.Body.String()
|
||||
c.Check(len(body), Not(Equals), 0)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestDbRequestTypes(c *C) {
|
||||
// Test dbRequestKind constants
|
||||
c.Check(acquiredb, Equals, dbRequestKind(0))
|
||||
c.Check(releasedb, Equals, dbRequestKind(1))
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestDbRequestStruct(c *C) {
|
||||
// Test dbRequest struct creation
|
||||
errCh := make(chan error, 1)
|
||||
req := dbRequest{
|
||||
kind: acquiredb,
|
||||
err: errCh,
|
||||
}
|
||||
|
||||
c.Check(req.kind, Equals, acquiredb)
|
||||
c.Check(req.err, NotNil)
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestAcquireAndReleaseDatabase(c *C) {
|
||||
// Initialize db requests channel
|
||||
initDBRequests()
|
||||
|
||||
// Test multiple acquire and release cycles
|
||||
for i := 0; i < 3; i++ {
|
||||
err := acquireDatabaseConnection()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
err = releaseDatabaseConnection()
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestConcurrentDatabaseAccess(c *C) {
|
||||
// Test concurrent database access
|
||||
done := make(chan bool, 10)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
go func(id int) {
|
||||
defer func() { done <- true }()
|
||||
|
||||
// Acquire and release database connection
|
||||
if err := acquireDatabaseConnection(); err == nil {
|
||||
// Simulate some work
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
_ = releaseDatabaseConnection()
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Wait for all goroutines to complete
|
||||
for i := 0; i < 10; i++ {
|
||||
<-done
|
||||
}
|
||||
|
||||
c.Check(true, Equals, true) // Test passed without deadlock
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestMaybeRunTaskInBackgroundWithError(c *C) {
|
||||
// Test task that returns an error
|
||||
w := httptest.NewRecorder()
|
||||
ginCtx, _ := gin.CreateTestContext(w)
|
||||
ginCtx.Request = httptest.NewRequest("GET", "/api/test", nil)
|
||||
|
||||
testErr := gin.Error{Type: gin.ErrorTypePublic, Err: gin.Error{}.Err}
|
||||
maybeRunTaskInBackground(ginCtx, "error-task", []string{}, func(out aptly.Progress, detail *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
return nil, testErr
|
||||
})
|
||||
|
||||
// Should return error status
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestMaybeRunTaskInBackgroundConflict(c *C) {
|
||||
// Test task with resource conflict
|
||||
w := httptest.NewRecorder()
|
||||
ginCtx, _ := gin.CreateTestContext(w)
|
||||
ginCtx.Request = httptest.NewRequest("GET", "/api/test", nil)
|
||||
|
||||
// Create two tasks with same resources to cause conflict
|
||||
resource := "test-resource-" + time.Now().Format("20060102150405")
|
||||
|
||||
// Start first task
|
||||
_, _ = runTaskInBackground("task1", []string{resource}, func(out aptly.Progress, detail *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
time.Sleep(100 * time.Millisecond) // Hold resource
|
||||
return &task.ProcessReturnValue{Code: 200}, nil
|
||||
})
|
||||
|
||||
// Try to start second task with same resource (should conflict)
|
||||
maybeRunTaskInBackground(ginCtx, "task2", []string{resource}, func(out aptly.Progress, detail *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
return &task.ProcessReturnValue{Code: 200}, nil
|
||||
})
|
||||
|
||||
// Should return 409 Conflict
|
||||
c.Check(w.Code, Equals, 409)
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestRunTaskInBackgroundWithNilReturn(c *C) {
|
||||
// Test task that returns nil ProcessReturnValue
|
||||
task, err := runTaskInBackground("nil-return-task", []string{}, func(out aptly.Progress, detail *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
return nil, nil
|
||||
})
|
||||
|
||||
c.Check(err, IsNil)
|
||||
c.Check(task, NotNil)
|
||||
|
||||
// Wait and clean up
|
||||
_, _ = s.context.TaskList().WaitForTaskByID(task.ID)
|
||||
_, _ = s.context.TaskList().DeleteTaskByID(task.ID)
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestMaybeRunTaskInBackgroundNilReturn(c *C) {
|
||||
// Test synchronous task with nil return value
|
||||
w := httptest.NewRecorder()
|
||||
ginCtx, _ := gin.CreateTestContext(w)
|
||||
ginCtx.Request = httptest.NewRequest("GET", "/api/test", nil)
|
||||
|
||||
maybeRunTaskInBackground(ginCtx, "nil-sync-task", []string{}, func(out aptly.Progress, detail *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
return nil, nil
|
||||
})
|
||||
|
||||
// Should return 200 with nil body
|
||||
c.Check(w.Code, Equals, 200)
|
||||
}
|
||||
@@ -1,267 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type ErrorTestSuite struct{}
|
||||
|
||||
var _ = Suite(&ErrorTestSuite{})
|
||||
|
||||
func (s *ErrorTestSuite) TestErrorStruct(c *C) {
|
||||
// Test Error struct creation and fields
|
||||
err := Error{Error: "test error message"}
|
||||
c.Check(err.Error, Equals, "test error message")
|
||||
}
|
||||
|
||||
func (s *ErrorTestSuite) TestErrorJSONMarshaling(c *C) {
|
||||
// Test JSON marshaling of Error struct
|
||||
err := Error{Error: "test error message"}
|
||||
|
||||
jsonData, marshalErr := json.Marshal(err)
|
||||
c.Check(marshalErr, IsNil)
|
||||
c.Check(string(jsonData), Equals, `{"error":"test error message"}`)
|
||||
}
|
||||
|
||||
func (s *ErrorTestSuite) TestErrorJSONUnmarshaling(c *C) {
|
||||
// Test JSON unmarshaling into Error struct
|
||||
jsonData := `{"error":"test error message"}`
|
||||
|
||||
var err Error
|
||||
unmarshalErr := json.Unmarshal([]byte(jsonData), &err)
|
||||
c.Check(unmarshalErr, IsNil)
|
||||
c.Check(err.Error, Equals, "test error message")
|
||||
}
|
||||
|
||||
func (s *ErrorTestSuite) TestErrorEmptyMessage(c *C) {
|
||||
// Test Error struct with empty message
|
||||
err := Error{Error: ""}
|
||||
c.Check(err.Error, Equals, "")
|
||||
|
||||
jsonData, marshalErr := json.Marshal(err)
|
||||
c.Check(marshalErr, IsNil)
|
||||
c.Check(string(jsonData), Equals, `{"error":""}`)
|
||||
}
|
||||
|
||||
func (s *ErrorTestSuite) TestErrorSpecialCharacters(c *C) {
|
||||
// Test Error struct with special characters
|
||||
specialMessages := []string{
|
||||
"error with \"quotes\"",
|
||||
"error with 'apostrophes'",
|
||||
"error with \n newlines",
|
||||
"error with \t tabs",
|
||||
"error with unicode: 你好",
|
||||
"error with emoji: 🚨❌",
|
||||
"error with backslashes: \\path\\to\\file",
|
||||
"error with json characters: {\"key\": \"value\"}",
|
||||
"error with < > & characters",
|
||||
"error with null \x00 character",
|
||||
}
|
||||
|
||||
for i, message := range specialMessages {
|
||||
err := Error{Error: message}
|
||||
c.Check(err.Error, Equals, message, Commentf("Test case %d", i))
|
||||
|
||||
// Test JSON marshaling works with special characters
|
||||
jsonData, marshalErr := json.Marshal(err)
|
||||
c.Check(marshalErr, IsNil, Commentf("Marshal failed for case %d: %s", i, message))
|
||||
|
||||
// Test JSON unmarshaling works with special characters
|
||||
var unmarshaled Error
|
||||
unmarshalErr := json.Unmarshal(jsonData, &unmarshaled)
|
||||
c.Check(unmarshalErr, IsNil, Commentf("Unmarshal failed for case %d: %s", i, message))
|
||||
c.Check(unmarshaled.Error, Equals, message, Commentf("Round-trip failed for case %d", i))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ErrorTestSuite) TestErrorLongMessage(c *C) {
|
||||
// Test Error struct with very long message
|
||||
longMessage := ""
|
||||
for i := 0; i < 1000; i++ {
|
||||
longMessage += "This is a very long error message. "
|
||||
}
|
||||
|
||||
err := Error{Error: longMessage}
|
||||
c.Check(err.Error, Equals, longMessage)
|
||||
|
||||
// Test JSON marshaling/unmarshaling with long message
|
||||
jsonData, marshalErr := json.Marshal(err)
|
||||
c.Check(marshalErr, IsNil)
|
||||
|
||||
var unmarshaled Error
|
||||
unmarshalErr := json.Unmarshal(jsonData, &unmarshaled)
|
||||
c.Check(unmarshalErr, IsNil)
|
||||
c.Check(unmarshaled.Error, Equals, longMessage)
|
||||
}
|
||||
|
||||
func (s *ErrorTestSuite) TestErrorJSONFieldName(c *C) {
|
||||
// Test that the JSON field name is exactly "error"
|
||||
err := Error{Error: "test"}
|
||||
|
||||
jsonData, marshalErr := json.Marshal(err)
|
||||
c.Check(marshalErr, IsNil)
|
||||
|
||||
// Parse as generic map to check field name
|
||||
var result map[string]interface{}
|
||||
unmarshalErr := json.Unmarshal(jsonData, &result)
|
||||
c.Check(unmarshalErr, IsNil)
|
||||
|
||||
// Check that the field is named "error"
|
||||
value, exists := result["error"]
|
||||
c.Check(exists, Equals, true)
|
||||
c.Check(value, Equals, "test")
|
||||
|
||||
// Check that no other fields exist
|
||||
c.Check(len(result), Equals, 1)
|
||||
}
|
||||
|
||||
func (s *ErrorTestSuite) TestErrorJSONWithExtraFields(c *C) {
|
||||
// Test unmarshaling JSON with extra fields (should be ignored)
|
||||
jsonData := `{"error":"test error","extra":"ignored","number":123}`
|
||||
|
||||
var err Error
|
||||
unmarshalErr := json.Unmarshal([]byte(jsonData), &err)
|
||||
c.Check(unmarshalErr, IsNil)
|
||||
c.Check(err.Error, Equals, "test error")
|
||||
}
|
||||
|
||||
func (s *ErrorTestSuite) TestErrorJSONMissingField(c *C) {
|
||||
// Test unmarshaling JSON missing the error field
|
||||
jsonData := `{"other":"value"}`
|
||||
|
||||
var err Error
|
||||
unmarshalErr := json.Unmarshal([]byte(jsonData), &err)
|
||||
c.Check(unmarshalErr, IsNil)
|
||||
c.Check(err.Error, Equals, "") // Should be zero value
|
||||
}
|
||||
|
||||
func (s *ErrorTestSuite) TestErrorJSONInvalidJSON(c *C) {
|
||||
// Test unmarshaling invalid JSON
|
||||
invalidJSONs := []string{
|
||||
`{"error":}`,
|
||||
`{"error": invalid}`,
|
||||
`{error: "missing quotes"}`,
|
||||
`{"error": "unterminated`,
|
||||
`malformed json`,
|
||||
``,
|
||||
`null`,
|
||||
`[]`,
|
||||
`123`,
|
||||
}
|
||||
|
||||
for i, jsonData := range invalidJSONs {
|
||||
var err Error
|
||||
unmarshalErr := json.Unmarshal([]byte(jsonData), &err)
|
||||
|
||||
// Should either error or handle gracefully
|
||||
if unmarshalErr == nil {
|
||||
// If no error, check the result is reasonable
|
||||
c.Check(err.Error, FitsTypeOf, "", Commentf("Invalid JSON case %d: %s", i, jsonData))
|
||||
} else {
|
||||
// Error is expected for malformed JSON
|
||||
c.Check(unmarshalErr, NotNil, Commentf("Expected error for case %d: %s", i, jsonData))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ErrorTestSuite) TestErrorZeroValue(c *C) {
|
||||
// Test zero value of Error struct
|
||||
var err Error
|
||||
c.Check(err.Error, Equals, "")
|
||||
|
||||
// Test JSON marshaling of zero value
|
||||
jsonData, marshalErr := json.Marshal(err)
|
||||
c.Check(marshalErr, IsNil)
|
||||
c.Check(string(jsonData), Equals, `{"error":""}`)
|
||||
}
|
||||
|
||||
func (s *ErrorTestSuite) TestErrorPointer(c *C) {
|
||||
// Test Error struct as pointer
|
||||
err := &Error{Error: "pointer error"}
|
||||
c.Check(err.Error, Equals, "pointer error")
|
||||
|
||||
// Test JSON marshaling of pointer
|
||||
jsonData, marshalErr := json.Marshal(err)
|
||||
c.Check(marshalErr, IsNil)
|
||||
c.Check(string(jsonData), Equals, `{"error":"pointer error"}`)
|
||||
|
||||
// Test JSON unmarshaling into pointer
|
||||
var err2 *Error
|
||||
unmarshalErr := json.Unmarshal(jsonData, &err2)
|
||||
c.Check(unmarshalErr, IsNil)
|
||||
c.Check(err2, NotNil)
|
||||
c.Check(err2.Error, Equals, "pointer error")
|
||||
}
|
||||
|
||||
func (s *ErrorTestSuite) TestErrorStructCopy(c *C) {
|
||||
// Test copying Error struct
|
||||
err1 := Error{Error: "original error"}
|
||||
err2 := err1
|
||||
|
||||
c.Check(err2.Error, Equals, "original error")
|
||||
|
||||
// Modify original and ensure copy is independent
|
||||
err1.Error = "modified error"
|
||||
c.Check(err1.Error, Equals, "modified error")
|
||||
c.Check(err2.Error, Equals, "original error")
|
||||
}
|
||||
|
||||
func (s *ErrorTestSuite) TestErrorStructComparison(c *C) {
|
||||
// Test comparing Error structs
|
||||
err1 := Error{Error: "same message"}
|
||||
err2 := Error{Error: "same message"}
|
||||
err3 := Error{Error: "different message"}
|
||||
|
||||
c.Check(err1 == err2, Equals, true)
|
||||
c.Check(err1 == err3, Equals, false)
|
||||
c.Check(err2 == err3, Equals, false)
|
||||
}
|
||||
|
||||
func (s *ErrorTestSuite) TestErrorStructInSlice(c *C) {
|
||||
// Test Error struct in slice operations
|
||||
errors := []Error{
|
||||
{Error: "first error"},
|
||||
{Error: "second error"},
|
||||
{Error: "third error"},
|
||||
}
|
||||
|
||||
c.Check(len(errors), Equals, 3)
|
||||
c.Check(errors[0].Error, Equals, "first error")
|
||||
c.Check(errors[1].Error, Equals, "second error")
|
||||
c.Check(errors[2].Error, Equals, "third error")
|
||||
|
||||
// Test JSON marshaling of slice
|
||||
jsonData, marshalErr := json.Marshal(errors)
|
||||
c.Check(marshalErr, IsNil)
|
||||
|
||||
var unmarshaled []Error
|
||||
unmarshalErr := json.Unmarshal(jsonData, &unmarshaled)
|
||||
c.Check(unmarshalErr, IsNil)
|
||||
c.Check(len(unmarshaled), Equals, 3)
|
||||
c.Check(unmarshaled[0].Error, Equals, "first error")
|
||||
}
|
||||
|
||||
func (s *ErrorTestSuite) TestErrorStructInMap(c *C) {
|
||||
// Test Error struct in map operations
|
||||
errorMap := map[string]Error{
|
||||
"key1": {Error: "first error"},
|
||||
"key2": {Error: "second error"},
|
||||
}
|
||||
|
||||
c.Check(len(errorMap), Equals, 2)
|
||||
c.Check(errorMap["key1"].Error, Equals, "first error")
|
||||
c.Check(errorMap["key2"].Error, Equals, "second error")
|
||||
|
||||
// Test JSON marshaling of map
|
||||
jsonData, marshalErr := json.Marshal(errorMap)
|
||||
c.Check(marshalErr, IsNil)
|
||||
|
||||
var unmarshaled map[string]Error
|
||||
unmarshalErr := json.Unmarshal(jsonData, &unmarshaled)
|
||||
c.Check(unmarshalErr, IsNil)
|
||||
c.Check(len(unmarshaled), Equals, 2)
|
||||
c.Check(unmarshaled["key1"].Error, Equals, "first error")
|
||||
c.Check(unmarshaled["key2"].Error, Equals, "second error")
|
||||
}
|
||||
+2
-2
@@ -122,7 +122,7 @@ func apiFilesUpload(c *gin.Context) {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = src.Close() }()
|
||||
defer src.Close()
|
||||
|
||||
destPath := filepath.Join(path, filepath.Base(file.Filename))
|
||||
dst, err := os.Create(destPath)
|
||||
@@ -130,7 +130,7 @@ func apiFilesUpload(c *gin.Context) {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = dst.Close() }()
|
||||
defer dst.Close()
|
||||
|
||||
_, err = io.Copy(dst, src)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,338 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
// Hook up gocheck into the "go test" runner.
|
||||
func TestFiles(t *testing.T) { TestingT(t) }
|
||||
|
||||
type FilesSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&FilesSuite{})
|
||||
|
||||
func (s *FilesSuite) SetUpTest(c *C) {
|
||||
s.APISuite.SetUpTest(c)
|
||||
}
|
||||
|
||||
func (s *FilesSuite) TearDownTest(c *C) {
|
||||
// Clean up any test files
|
||||
if s.context != nil {
|
||||
uploadPath := s.context.UploadPath()
|
||||
if uploadPath != "" {
|
||||
os.RemoveAll(uploadPath)
|
||||
}
|
||||
}
|
||||
s.APISuite.TearDownTest(c)
|
||||
}
|
||||
|
||||
func (s *FilesSuite) TestVerifyPath(c *C) {
|
||||
// Valid paths
|
||||
c.Check(verifyPath("valid-dir"), Equals, true)
|
||||
c.Check(verifyPath("valid/sub/dir"), Equals, true)
|
||||
c.Check(verifyPath("valid/../other"), Equals, true) // filepath.Clean normalizes to "other"
|
||||
|
||||
// Invalid paths
|
||||
c.Check(verifyPath(""), Equals, false) // Empty path becomes "."
|
||||
c.Check(verifyPath("../invalid"), Equals, false) // Contains ".."
|
||||
c.Check(verifyPath(".."), Equals, false) // Is ".."
|
||||
c.Check(verifyPath("."), Equals, false) // Is "."
|
||||
c.Check(verifyPath("./"), Equals, false) // Contains "."
|
||||
}
|
||||
|
||||
func (s *FilesSuite) TestVerifyDirValid(c *C) {
|
||||
// Create a test gin context
|
||||
w := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(w)
|
||||
ctx.Params = gin.Params{
|
||||
{Key: "dir", Value: "valid-dir"},
|
||||
}
|
||||
|
||||
result := verifyDir(ctx)
|
||||
c.Check(result, Equals, true)
|
||||
}
|
||||
|
||||
func (s *FilesSuite) TestVerifyDirInvalid(c *C) {
|
||||
// Create a test gin context
|
||||
w := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(w)
|
||||
ctx.Params = gin.Params{
|
||||
{Key: "dir", Value: "../invalid"},
|
||||
}
|
||||
|
||||
result := verifyDir(ctx)
|
||||
c.Check(result, Equals, false)
|
||||
c.Check(w.Code, Equals, 400)
|
||||
}
|
||||
|
||||
func (s *FilesSuite) TestApiFilesListDirs(c *C) {
|
||||
// Create upload directory for testing
|
||||
uploadPath := s.context.UploadPath()
|
||||
err := os.MkdirAll(filepath.Join(uploadPath, "test-dir"), 0755)
|
||||
c.Assert(err, IsNil)
|
||||
defer os.RemoveAll(uploadPath)
|
||||
|
||||
// Create test file
|
||||
f, err := os.Create(filepath.Join(uploadPath, "test-file.txt"))
|
||||
c.Assert(err, IsNil)
|
||||
f.Close()
|
||||
|
||||
// Create test request
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/api/files", nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Check response
|
||||
c.Check(w.Code, Equals, 200)
|
||||
var result []string
|
||||
err = json.Unmarshal(w.Body.Bytes(), &result)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(len(result), Equals, 1)
|
||||
c.Check(result[0], Equals, "test-dir")
|
||||
}
|
||||
|
||||
func (s *FilesSuite) TestApiFilesUpload(c *C) {
|
||||
// Create multipart form data
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
part, err := writer.CreateFormFile("file", "test.txt")
|
||||
c.Assert(err, IsNil)
|
||||
part.Write([]byte("test content"))
|
||||
writer.Close()
|
||||
|
||||
// Create test request
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("POST", "/api/files/testdir", body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Check response
|
||||
c.Check(w.Code, Equals, 200)
|
||||
|
||||
// Verify file was uploaded
|
||||
uploadPath := filepath.Join(s.context.UploadPath(), "testdir", "test.txt")
|
||||
_, err = os.Stat(uploadPath)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
// Clean up
|
||||
os.RemoveAll(filepath.Join(s.context.UploadPath(), "testdir"))
|
||||
}
|
||||
|
||||
func (s *FilesSuite) TestApiFilesListFiles(c *C) {
|
||||
// Create test directory and files
|
||||
testDir := filepath.Join(s.context.UploadPath(), "testdir")
|
||||
err := os.MkdirAll(testDir, 0755)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
// Create test files
|
||||
for i := 0; i < 3; i++ {
|
||||
f, err := os.Create(filepath.Join(testDir, fmt.Sprintf("test%d.txt", i)))
|
||||
c.Assert(err, IsNil)
|
||||
f.Close()
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/api/files/testdir", nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Check response
|
||||
c.Check(w.Code, Equals, 200)
|
||||
|
||||
var result []string
|
||||
err = json.Unmarshal(w.Body.Bytes(), &result)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(len(result), Equals, 3)
|
||||
|
||||
// Clean up
|
||||
os.RemoveAll(testDir)
|
||||
}
|
||||
|
||||
func (s *FilesSuite) TestApiFilesDeleteDir(c *C) {
|
||||
// Create test directory
|
||||
testDir := filepath.Join(s.context.UploadPath(), "testdir")
|
||||
err := os.MkdirAll(testDir, 0755)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
// Create test file in directory
|
||||
f, err := os.Create(filepath.Join(testDir, "test.txt"))
|
||||
c.Assert(err, IsNil)
|
||||
f.Close()
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("DELETE", "/api/files/testdir", nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Check response
|
||||
c.Check(w.Code, Equals, 200)
|
||||
|
||||
// Verify directory was deleted
|
||||
_, err = os.Stat(testDir)
|
||||
c.Assert(os.IsNotExist(err), Equals, true)
|
||||
}
|
||||
|
||||
func (s *FilesSuite) TestApiFilesDeleteFile(c *C) {
|
||||
// Create test directory and file
|
||||
testDir := filepath.Join(s.context.UploadPath(), "testdir")
|
||||
err := os.MkdirAll(testDir, 0755)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
testFile := filepath.Join(testDir, "test.txt")
|
||||
f, err := os.Create(testFile)
|
||||
c.Assert(err, IsNil)
|
||||
f.Write([]byte("test content"))
|
||||
f.Close()
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("DELETE", "/api/files/testdir/test.txt", nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Check response
|
||||
c.Check(w.Code, Equals, 200)
|
||||
|
||||
// Verify file was deleted
|
||||
_, err = os.Stat(testFile)
|
||||
c.Assert(os.IsNotExist(err), Equals, true)
|
||||
|
||||
// Clean up
|
||||
os.RemoveAll(testDir)
|
||||
}
|
||||
|
||||
func (s *FilesSuite) TestApiFilesDeleteFileInvalidPath(c *C) {
|
||||
// Create test request with invalid path
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("DELETE", "/api/files/testdir/../invalid", nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should reject with 404 (not found) or 400 (bad request)
|
||||
c.Check(w.Code == 400 || w.Code == 404, Equals, true)
|
||||
}
|
||||
|
||||
// Custom checker for file existence
|
||||
var testFileExists Checker = &fileExistsChecker{
|
||||
CheckerInfo: &CheckerInfo{Name: "testFileExists", Params: []string{"filename"}},
|
||||
}
|
||||
|
||||
type fileExistsChecker struct {
|
||||
*CheckerInfo
|
||||
}
|
||||
|
||||
func (checker *fileExistsChecker) Check(params []interface{}, names []string) (result bool, error string) {
|
||||
filename, ok := params[0].(string)
|
||||
if !ok {
|
||||
return false, "filename must be a string"
|
||||
}
|
||||
|
||||
_, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false, ""
|
||||
}
|
||||
return false, err.Error()
|
||||
}
|
||||
return true, ""
|
||||
}
|
||||
|
||||
// Test core API functions
|
||||
func (s *FilesSuite) TestApiVersion(c *C) {
|
||||
w := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(w)
|
||||
ctx.Request = httptest.NewRequest("GET", "/api/version", nil)
|
||||
|
||||
apiVersion(ctx)
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
c.Check(w.Body.String(), Matches, `.*"Version":.*`)
|
||||
}
|
||||
|
||||
func (s *FilesSuite) TestApiHealthy(c *C) {
|
||||
w := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(w)
|
||||
ctx.Request = httptest.NewRequest("GET", "/api/healthy", nil)
|
||||
|
||||
apiHealthy(ctx)
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
c.Check(w.Body.String(), Matches, `.*"Status":"Aptly is healthy".*`)
|
||||
}
|
||||
|
||||
func (s *FilesSuite) TestApiReadyWhenReady(c *C) {
|
||||
isReady := &atomic.Value{}
|
||||
isReady.Store(true)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(w)
|
||||
ctx.Request = httptest.NewRequest("GET", "/api/ready", nil)
|
||||
|
||||
apiReady(isReady)(ctx)
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
c.Check(w.Body.String(), Matches, `.*"Status":"Aptly is ready".*`)
|
||||
}
|
||||
|
||||
func (s *FilesSuite) TestApiReadyWhenNotReady(c *C) {
|
||||
isReady := &atomic.Value{}
|
||||
isReady.Store(false)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(w)
|
||||
ctx.Request = httptest.NewRequest("GET", "/api/ready", nil)
|
||||
|
||||
apiReady(isReady)(ctx)
|
||||
|
||||
c.Check(w.Code, Equals, 503)
|
||||
c.Check(w.Body.String(), Matches, `.*"Status":"Aptly is unavailable".*`)
|
||||
}
|
||||
|
||||
func (s *FilesSuite) TestApiReadyWithNil(c *C) {
|
||||
w := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(w)
|
||||
ctx.Request = httptest.NewRequest("GET", "/api/ready", nil)
|
||||
|
||||
apiReady(nil)(ctx)
|
||||
|
||||
c.Check(w.Code, Equals, 503)
|
||||
c.Check(w.Body.String(), Matches, `.*"Status":"Aptly is unavailable".*`)
|
||||
}
|
||||
|
||||
func (s *FilesSuite) TestTruthy(c *C) {
|
||||
// Test string values
|
||||
c.Check(truthy("yes"), Equals, true)
|
||||
c.Check(truthy("true"), Equals, true)
|
||||
c.Check(truthy("1"), Equals, true)
|
||||
c.Check(truthy("on"), Equals, true)
|
||||
c.Check(truthy("anything"), Equals, true)
|
||||
c.Check(truthy("n"), Equals, false)
|
||||
c.Check(truthy("no"), Equals, false)
|
||||
c.Check(truthy("f"), Equals, false)
|
||||
c.Check(truthy("false"), Equals, false)
|
||||
c.Check(truthy("0"), Equals, false)
|
||||
c.Check(truthy("off"), Equals, false)
|
||||
c.Check(truthy("NO"), Equals, false) // case insensitive
|
||||
c.Check(truthy("FALSE"), Equals, false) // case insensitive
|
||||
|
||||
// Test int values
|
||||
c.Check(truthy(1), Equals, true)
|
||||
c.Check(truthy(42), Equals, true)
|
||||
c.Check(truthy(-1), Equals, true)
|
||||
c.Check(truthy(0), Equals, false)
|
||||
|
||||
// Test bool values
|
||||
c.Check(truthy(true), Equals, true)
|
||||
c.Check(truthy(false), Equals, false)
|
||||
|
||||
// Test nil
|
||||
c.Check(truthy(nil), Equals, false)
|
||||
}
|
||||
+9
-17
@@ -13,34 +13,26 @@ import (
|
||||
)
|
||||
|
||||
type gpgAddKeyParams struct {
|
||||
// Keyserver, when downloading GpgKeyIDs
|
||||
Keyserver string `json:"Keyserver" example:"hkp://keyserver.ubuntu.com:80"`
|
||||
// GpgKeyIDs to download from Keyserver, comma separated list
|
||||
GpgKeyID string `json:"GpgKeyID" example:"EF0F382A1A7B6500,8B48AD6246925553"`
|
||||
// Armored gpg public ket, instead of downloading from keyserver
|
||||
GpgKeyArmor string `json:"GpgKeyArmor" example:""`
|
||||
// Keyring for adding the keys (default: trustedkeys.gpg)
|
||||
Keyring string `json:"Keyring" example:"trustedkeys.gpg"`
|
||||
|
||||
// Add ASCII armored gpg public key, do not download from keyserver
|
||||
GpgKeyArmor string `json:"GpgKeyArmor" example:""`
|
||||
|
||||
// Keyserver to download keys provided in `GpgKeyID`
|
||||
Keyserver string `json:"Keyserver" example:"hkp://keyserver.ubuntu.com:80"`
|
||||
// Keys do download from `Keyserver`, separated by space
|
||||
GpgKeyID string `json:"GpgKeyID" example:"EF0F382A1A7B6500 8B48AD6246925553"`
|
||||
}
|
||||
|
||||
// @Summary Add GPG Keys
|
||||
// @Description **Adds GPG keys to aptly keyring**
|
||||
// @Description
|
||||
// @Description Add GPG public keys for veryfing remote repositories for mirroring.
|
||||
// @Description
|
||||
// @Description Keys can be added in two ways:
|
||||
// @Description * By providing the ASCII armord key in `GpgKeyArmor` (leave Keyserver and GpgKeyID empty)
|
||||
// @Description * By providing a `Keyserver` and one or more key IDs in `GpgKeyID`, separated by space (leave GpgKeyArmor empty)
|
||||
// @Description
|
||||
// @Tags Mirrors
|
||||
// @Consume json
|
||||
// @Param request body gpgAddKeyParams true "Parameters"
|
||||
// @Produce json
|
||||
// @Success 200 {object} string "OK"
|
||||
// @Failure 400 {object} Error "Bad Request"
|
||||
// @Router /api/gpg/key [post]
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Router /api/gpg [post]
|
||||
func apiGPGAddKey(c *gin.Context) {
|
||||
b := gpgAddKeyParams{}
|
||||
if c.Bind(&b) != nil {
|
||||
@@ -68,7 +60,7 @@ func apiGPGAddKey(c *gin.Context) {
|
||||
AbortWithJSONError(c, 400, err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = os.RemoveAll(tempdir) }()
|
||||
defer os.RemoveAll(tempdir)
|
||||
|
||||
keypath := filepath.Join(tempdir, "key")
|
||||
keyfile, e := os.Create(keypath)
|
||||
|
||||
-213
@@ -1,213 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type GPGTestSuite struct {
|
||||
router *gin.Engine
|
||||
}
|
||||
|
||||
var _ = Suite(&GPGTestSuite{})
|
||||
|
||||
func (s *GPGTestSuite) SetUpTest(c *C) {
|
||||
s.router = gin.New()
|
||||
s.router.POST("/api/gpg/key", apiGPGAddKey)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
}
|
||||
|
||||
func (s *GPGTestSuite) TestGPGAddKeyStructure(c *C) {
|
||||
// Test GPG key add endpoint structure with sample key data
|
||||
keyData := `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
Version: GnuPG v1
|
||||
|
||||
mQINBFKuaIQBEAC+JC5od6Vw1tz2SEfBE7tBLQhNy3z2SIu7iNC3Bi/W6xUy5YKw
|
||||
sample key data for testing
|
||||
-----END PGP PUBLIC KEY BLOCK-----`
|
||||
|
||||
req, _ := http.NewRequest("POST", "/api/gpg/key", bytes.NewBufferString(keyData))
|
||||
req.Header.Set("Content-Type", "application/pgp-keys")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will likely error due to no context or invalid key, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *GPGTestSuite) TestGPGAddKeyEmptyBody(c *C) {
|
||||
// Test GPG key add with empty body
|
||||
req, _ := http.NewRequest("POST", "/api/gpg/key", bytes.NewBufferString(""))
|
||||
req.Header.Set("Content-Type", "application/pgp-keys")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle empty body gracefully
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *GPGTestSuite) TestGPGAddKeyInvalidData(c *C) {
|
||||
// Test GPG key add with invalid key data
|
||||
invalidKeys := []string{
|
||||
"not a pgp key",
|
||||
"-----BEGIN PGP PUBLIC KEY BLOCK-----\ninvalid\n-----END PGP PUBLIC KEY BLOCK-----",
|
||||
"random text data",
|
||||
"<xml>not a key</xml>",
|
||||
"-----BEGIN CERTIFICATE-----\ninvalid cert\n-----END CERTIFICATE-----",
|
||||
}
|
||||
|
||||
for _, keyData := range invalidKeys {
|
||||
req, _ := http.NewRequest("POST", "/api/gpg/key", bytes.NewBufferString(keyData))
|
||||
req.Header.Set("Content-Type", "application/pgp-keys")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle invalid key data gracefully without crashing
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Key data: %s", keyData[:min(len(keyData), 50)]))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GPGTestSuite) TestGPGAddKeyHTTPMethods(c *C) {
|
||||
// Test that only POST method is allowed
|
||||
deniedMethods := []string{"GET", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"}
|
||||
|
||||
for _, method := range deniedMethods {
|
||||
req, _ := http.NewRequest(method, "/api/gpg/key", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 404, Commentf("Method: %s should be denied", method))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GPGTestSuite) TestGPGAddKeyContentTypes(c *C) {
|
||||
// Test different content types
|
||||
contentTypes := []string{
|
||||
"application/pgp-keys",
|
||||
"text/plain",
|
||||
"application/x-pgp-message",
|
||||
"application/octet-stream",
|
||||
"",
|
||||
}
|
||||
|
||||
keyData := "-----BEGIN PGP PUBLIC KEY BLOCK-----\nsample\n-----END PGP PUBLIC KEY BLOCK-----"
|
||||
|
||||
for _, contentType := range contentTypes {
|
||||
req, _ := http.NewRequest("POST", "/api/gpg/key", bytes.NewBufferString(keyData))
|
||||
if contentType != "" {
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle different content types without crashing
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Content-Type: %s", contentType))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GPGTestSuite) TestGPGAddKeyLargePayload(c *C) {
|
||||
// Test with large payload (simulate large key file)
|
||||
largeKeyData := "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
|
||||
for i := 0; i < 1000; i++ {
|
||||
largeKeyData += "large key data line " + string(rune(i)) + "\n"
|
||||
}
|
||||
largeKeyData += "-----END PGP PUBLIC KEY BLOCK-----"
|
||||
|
||||
req, _ := http.NewRequest("POST", "/api/gpg/key", bytes.NewBufferString(largeKeyData))
|
||||
req.Header.Set("Content-Type", "application/pgp-keys")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle large payloads without crashing
|
||||
c.Check(w.Code, Not(Equals), 0)
|
||||
}
|
||||
|
||||
func (s *GPGTestSuite) TestGPGAddKeyBinaryData(c *C) {
|
||||
// Test with binary data
|
||||
binaryData := []byte{0x00, 0x01, 0x02, 0x03, 0xFF, 0xFE, 0xFD}
|
||||
|
||||
req, _ := http.NewRequest("POST", "/api/gpg/key", bytes.NewBuffer(binaryData))
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle binary data without crashing
|
||||
c.Check(w.Code, Not(Equals), 0)
|
||||
}
|
||||
|
||||
func (s *GPGTestSuite) TestGPGAddKeySpecialCharacters(c *C) {
|
||||
// Test with special characters and encoding
|
||||
specialKeys := []string{
|
||||
"-----BEGIN PGP PUBLIC KEY BLOCK-----\nключ с русскими символами\n-----END PGP PUBLIC KEY BLOCK-----",
|
||||
"-----BEGIN PGP PUBLIC KEY BLOCK-----\n中文字符测试\n-----END PGP PUBLIC KEY BLOCK-----",
|
||||
"-----BEGIN PGP PUBLIC KEY BLOCK-----\n🔑 emoji key 🔐\n-----END PGP PUBLIC KEY BLOCK-----",
|
||||
"-----BEGIN PGP PUBLIC KEY BLOCK-----\n\"quotes\" and 'apostrophes'\n-----END PGP PUBLIC KEY BLOCK-----",
|
||||
"-----BEGIN PGP PUBLIC KEY BLOCK-----\n<>&\"'`\n-----END PGP PUBLIC KEY BLOCK-----",
|
||||
}
|
||||
|
||||
for i, keyData := range specialKeys {
|
||||
req, _ := http.NewRequest("POST", "/api/gpg/key", bytes.NewBufferString(keyData))
|
||||
req.Header.Set("Content-Type", "application/pgp-keys; charset=utf-8")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle special characters without crashing
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Special key test #%d", i+1))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GPGTestSuite) TestGPGAddKeyErrorHandling(c *C) {
|
||||
// Test various error conditions
|
||||
errorTests := []struct {
|
||||
description string
|
||||
data string
|
||||
contentType string
|
||||
expectError bool
|
||||
}{
|
||||
{"Empty key", "", "application/pgp-keys", true},
|
||||
{"Malformed header", "-----BEGIN WRONG BLOCK-----\ndata\n-----END WRONG BLOCK-----", "application/pgp-keys", true},
|
||||
{"Missing end", "-----BEGIN PGP PUBLIC KEY BLOCK-----\ndata", "application/pgp-keys", true},
|
||||
{"Missing begin", "data\n-----END PGP PUBLIC KEY BLOCK-----", "application/pgp-keys", true},
|
||||
{"Only whitespace", " \n\t\r\n ", "application/pgp-keys", true},
|
||||
{"JSON data", `{"key": "value"}`, "application/json", true},
|
||||
{"XML data", `<key>value</key>`, "application/xml", true},
|
||||
}
|
||||
|
||||
for _, test := range errorTests {
|
||||
req, _ := http.NewRequest("POST", "/api/gpg/key", bytes.NewBufferString(test.data))
|
||||
req.Header.Set("Content-Type", test.contentType)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle errors gracefully without crashing
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Test: %s", test.description))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GPGTestSuite) TestGPGAddKeyReliability(c *C) {
|
||||
// Test multiple sequential calls for reliability
|
||||
keyData := "-----BEGIN PGP PUBLIC KEY BLOCK-----\ntest key data\n-----END PGP PUBLIC KEY BLOCK-----"
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
req, _ := http.NewRequest("POST", "/api/gpg/key", bytes.NewBufferString(keyData))
|
||||
req.Header.Set("Content-Type", "application/pgp-keys")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should be consistent across multiple calls
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Call #%d", i+1))
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function for minimum of two integers
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
@@ -1,381 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type GraphTestSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&GraphTestSuite{})
|
||||
|
||||
func (s *GraphTestSuite) SetUpTest(c *C) {
|
||||
s.APISuite.SetUpTest(c)
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphDotFormat(c *C) {
|
||||
// Test requesting raw DOT format
|
||||
req, _ := http.NewRequest("GET", "/api/graph.dot", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should succeed with context and return DOT format
|
||||
c.Check(w.Code, Equals, 200)
|
||||
c.Check(w.Header().Get("Content-Type"), Equals, "text/plain; charset=utf-8")
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphGvFormat(c *C) {
|
||||
// Test requesting GV format (alias for DOT)
|
||||
req, _ := http.NewRequest("GET", "/api/graph.gv", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should succeed with context and return DOT format (gv is alias)
|
||||
c.Check(w.Code, Equals, 200)
|
||||
c.Check(w.Header().Get("Content-Type"), Equals, "text/plain; charset=utf-8")
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphSvgFormat(c *C) {
|
||||
// Test requesting SVG format (requires graphviz)
|
||||
req, _ := http.NewRequest("GET", "/api/graph.svg", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will likely error due to no context or missing graphviz
|
||||
c.Check(w.Code, Not(Equals), 200) // Expect error
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphPngFormat(c *C) {
|
||||
// Test requesting PNG format (requires graphviz)
|
||||
req, _ := http.NewRequest("GET", "/api/graph.png", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will likely error due to no context or missing graphviz
|
||||
c.Check(w.Code, Not(Equals), 200) // Expect error
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphWithHorizontalLayout(c *C) {
|
||||
// Test with horizontal layout parameter
|
||||
req, _ := http.NewRequest("GET", "/api/graph.svg?layout=horizontal", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will likely error due to no context, but should parse layout parameter
|
||||
c.Check(w.Code, Not(Equals), 200) // Expect error due to missing context
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphWithVerticalLayout(c *C) {
|
||||
// Test with vertical layout parameter
|
||||
req, _ := http.NewRequest("GET", "/api/graph.png?layout=vertical", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will likely error due to no context, but should parse layout parameter
|
||||
c.Check(w.Code, Not(Equals), 200) // Expect error due to missing context
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphWithInvalidLayout(c *C) {
|
||||
// Test with invalid layout parameter
|
||||
req, _ := http.NewRequest("GET", "/api/graph.dot?layout=invalid", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should succeed - invalid layout is ignored
|
||||
c.Check(w.Code, Equals, 200)
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphWithEmptyLayout(c *C) {
|
||||
// Test with empty layout parameter
|
||||
req, _ := http.NewRequest("GET", "/api/graph.svg?layout=", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will fail because SVG requires graphviz which is not installed
|
||||
c.Check(w.Code, Equals, 500)
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphWithMultipleParams(c *C) {
|
||||
// Test with multiple query parameters
|
||||
req, _ := http.NewRequest("GET", "/api/graph.png?layout=vertical&extra=param&another=value", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will fail because PNG requires graphviz which is not installed
|
||||
c.Check(w.Code, Equals, 500)
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphParameterHandling(c *C) {
|
||||
// Test parameter extraction and validation
|
||||
testCases := []struct {
|
||||
path string
|
||||
description string
|
||||
}{
|
||||
{"/api/graph.dot", "DOT format"},
|
||||
{"/api/graph.gv", "GV format"},
|
||||
{"/api/graph.svg", "SVG format"},
|
||||
{"/api/graph.png", "PNG format"},
|
||||
{"/api/graph.pdf", "PDF format"},
|
||||
{"/api/graph.ps", "PostScript format"},
|
||||
{"/api/graph.jpg", "JPEG format"},
|
||||
{"/api/graph.gif", "GIF format"},
|
||||
{"/api/graph.unknown", "Unknown format"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
req, _ := http.NewRequest("GET", tc.path, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// All should return some response without crashing
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Test case: %s", tc.description))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphMimeTypeHandling(c *C) {
|
||||
// Test MIME type detection for different extensions
|
||||
extensions := map[string]string{
|
||||
"svg": "image/svg+xml",
|
||||
"png": "image/png",
|
||||
"pdf": "application/pdf",
|
||||
"ps": "application/postscript",
|
||||
"jpg": "image/jpeg",
|
||||
"gif": "image/gif",
|
||||
}
|
||||
|
||||
for ext, expectedMime := range extensions {
|
||||
actualMime := mime.TypeByExtension("." + ext)
|
||||
if actualMime != "" {
|
||||
// Just check that the actual MIME type starts with expected
|
||||
c.Check(strings.HasPrefix(actualMime, expectedMime), Equals, true,
|
||||
Commentf("MIME type mismatch for extension: %s", ext))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphHTTPMethods(c *C) {
|
||||
// Test that only GET method is allowed
|
||||
deniedMethods := []string{"POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"}
|
||||
|
||||
for _, method := range deniedMethods {
|
||||
req, _ := http.NewRequest(method, "/api/graph.svg", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 404, Commentf("Method: %s should be denied", method))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphPathValidation(c *C) {
|
||||
// Test path validation and parameter extraction
|
||||
validPaths := []string{
|
||||
"/api/graph.dot",
|
||||
"/api/graph.svg",
|
||||
"/api/graph.png",
|
||||
"/api/graph.pdf",
|
||||
}
|
||||
|
||||
invalidPaths := []string{
|
||||
"/api/graph", // Missing extension
|
||||
"/api/graph.", // Empty extension
|
||||
"/api/graphs.svg", // Wrong endpoint name
|
||||
}
|
||||
|
||||
for _, path := range validPaths {
|
||||
req, _ := http.NewRequest("GET", path, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should match route (even if it errors due to missing context)
|
||||
c.Check(w.Code, Not(Equals), 404, Commentf("Valid path should match route: %s", path))
|
||||
}
|
||||
|
||||
for _, path := range invalidPaths {
|
||||
req, _ := http.NewRequest("GET", path, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should not match route
|
||||
c.Check(w.Code, Equals, 404, Commentf("Invalid path should not match route: %s", path))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphExtensionExtraction(c *C) {
|
||||
// Test that extension is properly extracted from path
|
||||
testPaths := []string{
|
||||
"/api/graph.dot",
|
||||
"/api/graph.svg",
|
||||
"/api/graph.png",
|
||||
"/api/graph.pdf",
|
||||
"/api/graph.ps",
|
||||
"/api/graph.jpg",
|
||||
"/api/graph.gif",
|
||||
"/api/graph.unknown",
|
||||
}
|
||||
|
||||
for _, path := range testPaths {
|
||||
req, _ := http.NewRequest("GET", path, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle extension extraction without crashing
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Extension extraction failed for: %s", path))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphQueryParameterHandling(c *C) {
|
||||
// Test various query parameter combinations
|
||||
queryTests := []struct {
|
||||
query string
|
||||
description string
|
||||
}{
|
||||
{"", "no parameters"},
|
||||
{"layout=horizontal", "horizontal layout"},
|
||||
{"layout=vertical", "vertical layout"},
|
||||
{"layout=invalid", "invalid layout"},
|
||||
{"layout=", "empty layout"},
|
||||
{"layout=horizontal&extra=param", "multiple parameters"},
|
||||
{"unknown=param", "unknown parameter"},
|
||||
{"layout=horizontal&layout=vertical", "duplicate parameters"},
|
||||
}
|
||||
|
||||
for _, test := range queryTests {
|
||||
path := "/api/graph.svg"
|
||||
if test.query != "" {
|
||||
path += "?" + test.query
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest("GET", path, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle query parameters without crashing
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Query parameter test: %s", test.description))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphErrorHandling(c *C) {
|
||||
// Test various error conditions
|
||||
errorTests := []struct {
|
||||
path string
|
||||
description string
|
||||
}{
|
||||
{"/api/graph.svg", "missing database context"},
|
||||
{"/api/graph.png", "missing graphviz"},
|
||||
{"/api/graph.unknown", "unknown format"},
|
||||
{"/api/graph.dot", "raw DOT format"},
|
||||
}
|
||||
|
||||
for _, test := range errorTests {
|
||||
req, _ := http.NewRequest("GET", test.path, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle errors gracefully without panicking
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Error test: %s", test.description))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphContentTypeHeaders(c *C) {
|
||||
// Test that appropriate content types are set for different formats
|
||||
formatTests := []struct {
|
||||
ext string
|
||||
expectJSON bool
|
||||
expectImage bool
|
||||
}{
|
||||
{"dot", false, false}, // Should return text
|
||||
{"gv", false, false}, // Should return text
|
||||
{"svg", false, true}, // Should return image/svg+xml (if successful)
|
||||
{"png", false, true}, // Should return image/png (if successful)
|
||||
{"pdf", false, false}, // Should return application/pdf (if successful)
|
||||
}
|
||||
|
||||
for _, test := range formatTests {
|
||||
req, _ := http.NewRequest("GET", "/api/graph."+test.ext, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
contentType := w.Header().Get("Content-Type")
|
||||
|
||||
if test.expectJSON {
|
||||
c.Check(strings.Contains(contentType, "application/json"), Equals, true,
|
||||
Commentf("Expected JSON content type for .%s, got: %s", test.ext, contentType))
|
||||
}
|
||||
|
||||
// Note: Image content types will only be set if graphviz is available and context exists
|
||||
c.Check(contentType, Not(Equals), "", Commentf("Content type should be set for .%s", test.ext))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphSpecialCharacters(c *C) {
|
||||
// Test handling of special characters in query parameters
|
||||
specialQueries := []string{
|
||||
"layout=horizontal%20with%20spaces",
|
||||
"layout=vertical¶m=value%20with%20spaces",
|
||||
"layout=test%26special%3Dchars",
|
||||
"layout=unicode%E2%9C%93",
|
||||
"param=%3Cscript%3Ealert%28%29%3C%2Fscript%3E", // XSS attempt
|
||||
}
|
||||
|
||||
for _, query := range specialQueries {
|
||||
req, _ := http.NewRequest("GET", "/api/graph.svg?"+query, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle special characters without crashing
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Special character test failed for: %s", query))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphLargeExtensions(c *C) {
|
||||
// Test with very long extensions
|
||||
longExt := strings.Repeat("x", 1000)
|
||||
req, _ := http.NewRequest("GET", "/api/graph."+longExt, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle long extensions without crashing
|
||||
c.Check(w.Code, Not(Equals), 0)
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphReliability(c *C) {
|
||||
// Test multiple sequential calls for reliability
|
||||
for i := 0; i < 5; i++ {
|
||||
req, _ := http.NewRequest("GET", "/api/graph.dot", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should be consistent across multiple calls
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Call #%d", i+1))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphConcurrency(c *C) {
|
||||
// Test concurrent requests to ensure thread safety
|
||||
done := make(chan bool, 5)
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
go func(id int) {
|
||||
defer func() { done <- true }()
|
||||
|
||||
req, _ := http.NewRequest("GET", "/api/graph.svg", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle concurrent requests without issues
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Wait for all requests to complete
|
||||
for i := 0; i < 5; i++ {
|
||||
<-done
|
||||
}
|
||||
|
||||
c.Check(true, Equals, true) // Test completed without deadlocks
|
||||
}
|
||||
@@ -1,600 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type MetricsTestSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&MetricsTestSuite{})
|
||||
|
||||
func (s *MetricsTestSuite) SetUpTest(c *C) {
|
||||
s.APISuite.SetUpTest(c)
|
||||
// Reset metrics registrar state for each test
|
||||
MetricsCollectorRegistrar.hasRegistered = false
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestMetricsCollectorRegistrarRegisterOnce(c *C) {
|
||||
// Test that metrics are only registered once
|
||||
registrar := &metricsCollectorRegistrar{hasRegistered: false}
|
||||
|
||||
// First registration should work
|
||||
registrar.Register(s.router.(*gin.Engine))
|
||||
c.Check(registrar.hasRegistered, Equals, true)
|
||||
|
||||
// Second registration should be skipped
|
||||
registrar.Register(s.router.(*gin.Engine))
|
||||
c.Check(registrar.hasRegistered, Equals, true)
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestMetricsCollectorRegistrarVersionGauge(c *C) {
|
||||
// Test that version gauge is set correctly
|
||||
registrar := &metricsCollectorRegistrar{hasRegistered: false}
|
||||
|
||||
// Register metrics
|
||||
registrar.Register(s.router.(*gin.Engine))
|
||||
|
||||
// Check that version gauge was set
|
||||
expectedLabels := prometheus.Labels{
|
||||
"version": aptly.Version,
|
||||
"goversion": runtime.Version(),
|
||||
}
|
||||
|
||||
gauge := apiVersionGauge.With(expectedLabels)
|
||||
c.Check(gauge, NotNil)
|
||||
|
||||
// Verify the gauge value is 1
|
||||
metric := &dto.Metric{}
|
||||
gauge.(prometheus.Gauge).Write(metric)
|
||||
c.Check(metric.GetGauge().GetValue(), Equals, float64(1))
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestApiRequestsInFlightGauge(c *C) {
|
||||
// Test that in-flight requests gauge works
|
||||
c.Check(apiRequestsInFlightGauge, NotNil)
|
||||
|
||||
// Test that we can create labels for the gauge
|
||||
gauge := apiRequestsInFlightGauge.WithLabelValues("GET", "/api/test")
|
||||
c.Check(gauge, NotNil)
|
||||
|
||||
// Test incrementing and decrementing
|
||||
gauge.Inc()
|
||||
gauge.Dec()
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestApiRequestsTotalCounter(c *C) {
|
||||
// Test that total requests counter works
|
||||
c.Check(apiRequestsTotalCounter, NotNil)
|
||||
|
||||
// Test that we can create labels for the counter
|
||||
counter := apiRequestsTotalCounter.WithLabelValues("200", "GET", "/api/test")
|
||||
c.Check(counter, NotNil)
|
||||
|
||||
// Test incrementing
|
||||
counter.Inc()
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestApiRequestSizeSummary(c *C) {
|
||||
// Test that request size summary works
|
||||
c.Check(apiRequestSizeSummary, NotNil)
|
||||
|
||||
// Test that we can create labels for the summary
|
||||
summary := apiRequestSizeSummary.WithLabelValues("200", "POST", "/api/test")
|
||||
c.Check(summary, NotNil)
|
||||
|
||||
// Test observing values
|
||||
summary.Observe(1024.0)
|
||||
summary.Observe(512.0)
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestApiResponseSizeSummary(c *C) {
|
||||
// Test that response size summary works
|
||||
c.Check(apiResponseSizeSummary, NotNil)
|
||||
|
||||
// Test that we can create labels for the summary
|
||||
summary := apiResponseSizeSummary.WithLabelValues("200", "GET", "/api/test")
|
||||
c.Check(summary, NotNil)
|
||||
|
||||
// Test observing values
|
||||
summary.Observe(2048.0)
|
||||
summary.Observe(1024.0)
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestApiRequestsDurationSummary(c *C) {
|
||||
// Test that request duration summary works
|
||||
c.Check(apiRequestsDurationSummary, NotNil)
|
||||
|
||||
// Test that we can create labels for the summary
|
||||
summary := apiRequestsDurationSummary.WithLabelValues("200", "GET", "/api/test")
|
||||
c.Check(summary, NotNil)
|
||||
|
||||
// Test observing duration values
|
||||
summary.Observe(0.1) // 100ms
|
||||
summary.Observe(0.05) // 50ms
|
||||
summary.Observe(1.0) // 1s
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestApiFilesUploadedCounter(c *C) {
|
||||
// Test that files uploaded counter works
|
||||
c.Check(apiFilesUploadedCounter, NotNil)
|
||||
|
||||
// Test that we can create labels for the counter
|
||||
counter := apiFilesUploadedCounter.WithLabelValues("uploads")
|
||||
c.Check(counter, NotNil)
|
||||
|
||||
// Test incrementing
|
||||
counter.Inc()
|
||||
counter.Add(5)
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestApiReposPackageCountGauge(c *C) {
|
||||
// Test that repos package count gauge works
|
||||
c.Check(apiReposPackageCountGauge, NotNil)
|
||||
|
||||
// Test that we can create labels for the gauge
|
||||
gauge := apiReposPackageCountGauge.WithLabelValues("source", "stable", "main")
|
||||
c.Check(gauge, NotNil)
|
||||
|
||||
// Test setting values
|
||||
gauge.Set(100)
|
||||
gauge.Set(150)
|
||||
gauge.Inc()
|
||||
gauge.Dec()
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestMetricsPrometheusIntegration(c *C) {
|
||||
// Test integration with Prometheus client library
|
||||
|
||||
// Test that metrics are properly registered with default registry
|
||||
metricNames := []string{
|
||||
"aptly_api_http_requests_in_flight",
|
||||
"aptly_api_http_requests_total",
|
||||
"aptly_api_http_request_size_bytes",
|
||||
"aptly_api_http_response_size_bytes",
|
||||
"aptly_api_http_request_duration_seconds",
|
||||
"aptly_build_info",
|
||||
"aptly_api_files_uploaded_total",
|
||||
"aptly_repos_package_count",
|
||||
}
|
||||
|
||||
for _, metricName := range metricNames {
|
||||
// Try to gather metrics to ensure they're registered
|
||||
gathered, err := prometheus.DefaultGatherer.Gather()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
found := false
|
||||
for _, metricFamily := range gathered {
|
||||
if metricFamily.GetName() == metricName {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
c.Check(found, Equals, true, Commentf("Metric %s not found", metricName))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestMetricsLabels(c *C) {
|
||||
// Test that metrics have expected labels
|
||||
|
||||
// Test in-flight gauge labels
|
||||
gauge := apiRequestsInFlightGauge.WithLabelValues("GET", "/api/test")
|
||||
c.Check(gauge, NotNil)
|
||||
|
||||
// Test total counter labels
|
||||
counter := apiRequestsTotalCounter.WithLabelValues("200", "GET", "/api/test")
|
||||
c.Check(counter, NotNil)
|
||||
|
||||
// Test request size summary labels
|
||||
requestSummary := apiRequestSizeSummary.WithLabelValues("200", "POST", "/api/upload")
|
||||
c.Check(requestSummary, NotNil)
|
||||
|
||||
// Test response size summary labels
|
||||
responseSummary := apiResponseSizeSummary.WithLabelValues("404", "GET", "/api/missing")
|
||||
c.Check(responseSummary, NotNil)
|
||||
|
||||
// Test duration summary labels
|
||||
durationSummary := apiRequestsDurationSummary.WithLabelValues("500", "POST", "/api/error")
|
||||
c.Check(durationSummary, NotNil)
|
||||
|
||||
// Test version gauge labels
|
||||
versionGauge := apiVersionGauge.WithLabelValues("1.0.0", "go1.19")
|
||||
c.Check(versionGauge, NotNil)
|
||||
|
||||
// Test files uploaded counter labels
|
||||
filesCounter := apiFilesUploadedCounter.WithLabelValues("temp-uploads")
|
||||
c.Check(filesCounter, NotNil)
|
||||
|
||||
// Test repos package count gauge labels
|
||||
reposGauge := apiReposPackageCountGauge.WithLabelValues("snapshot:test", "testing", "contrib")
|
||||
c.Check(reposGauge, NotNil)
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestMetricsWithDifferentHTTPCodes(c *C) {
|
||||
// Test metrics with various HTTP status codes
|
||||
httpCodes := []string{"200", "201", "400", "401", "403", "404", "409", "500", "502", "503"}
|
||||
|
||||
for _, code := range httpCodes {
|
||||
// Test that metrics work with different status codes
|
||||
counter := apiRequestsTotalCounter.WithLabelValues(code, "GET", "/api/test")
|
||||
counter.Inc()
|
||||
|
||||
requestSummary := apiRequestSizeSummary.WithLabelValues(code, "POST", "/api/test")
|
||||
requestSummary.Observe(100)
|
||||
|
||||
responseSummary := apiResponseSizeSummary.WithLabelValues(code, "GET", "/api/test")
|
||||
responseSummary.Observe(200)
|
||||
|
||||
durationSummary := apiRequestsDurationSummary.WithLabelValues(code, "PUT", "/api/test")
|
||||
durationSummary.Observe(0.1)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestMetricsWithDifferentHTTPMethods(c *C) {
|
||||
// Test metrics with various HTTP methods
|
||||
httpMethods := []string{"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"}
|
||||
|
||||
for _, method := range httpMethods {
|
||||
// Test that metrics work with different HTTP methods
|
||||
gauge := apiRequestsInFlightGauge.WithLabelValues(method, "/api/test")
|
||||
gauge.Inc()
|
||||
gauge.Dec()
|
||||
|
||||
counter := apiRequestsTotalCounter.WithLabelValues("200", method, "/api/test")
|
||||
counter.Inc()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestMetricsWithDifferentPaths(c *C) {
|
||||
// Test metrics with various API paths
|
||||
apiPaths := []string{
|
||||
"/api/repos",
|
||||
"/api/repos/test",
|
||||
"/api/snapshots",
|
||||
"/api/publish",
|
||||
"/api/files",
|
||||
"/api/files/upload",
|
||||
"/api/mirrors",
|
||||
"/api/tasks",
|
||||
"/api/version",
|
||||
}
|
||||
|
||||
for _, path := range apiPaths {
|
||||
counter := apiRequestsTotalCounter.WithLabelValues("200", "GET", path)
|
||||
counter.Inc()
|
||||
|
||||
gauge := apiRequestsInFlightGauge.WithLabelValues("GET", path)
|
||||
gauge.Inc()
|
||||
gauge.Dec()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestMetricsThreadSafety(c *C) {
|
||||
// Test that metrics are thread-safe by simulating concurrent access
|
||||
done := make(chan bool, 10)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
go func(id int) {
|
||||
defer func() { done <- true }()
|
||||
|
||||
// Simulate concurrent metric updates
|
||||
for j := 0; j < 100; j++ {
|
||||
counter := apiRequestsTotalCounter.WithLabelValues("200", "GET", "/api/concurrent")
|
||||
counter.Inc()
|
||||
|
||||
gauge := apiRequestsInFlightGauge.WithLabelValues("GET", "/api/concurrent")
|
||||
gauge.Inc()
|
||||
gauge.Dec()
|
||||
|
||||
summary := apiRequestsDurationSummary.WithLabelValues("200", "GET", "/api/concurrent")
|
||||
summary.Observe(0.01)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Wait for all goroutines to complete
|
||||
for i := 0; i < 10; i++ {
|
||||
<-done
|
||||
}
|
||||
|
||||
// Verify metrics were updated (exact count doesn't matter due to concurrency)
|
||||
c.Check(true, Equals, true) // Test completed without race conditions
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestMetricsMetadata(c *C) {
|
||||
// Test that metrics have proper metadata (help text, names)
|
||||
|
||||
// Gather all metrics
|
||||
gathered, err := prometheus.DefaultGatherer.Gather()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
expectedMetrics := map[string]string{
|
||||
"aptly_api_http_requests_in_flight": "Number of concurrent HTTP api requests currently handled.",
|
||||
"aptly_api_http_requests_total": "Total number of api requests.",
|
||||
"aptly_api_http_request_size_bytes": "Api HTTP request size in bytes.",
|
||||
"aptly_api_http_response_size_bytes": "Api HTTP response size in bytes.",
|
||||
"aptly_api_http_request_duration_seconds": "Duration of api requests in seconds.",
|
||||
"aptly_build_info": "Metric with a constant '1' value labeled by version and goversion from which aptly was built.",
|
||||
"aptly_api_files_uploaded_total": "Total number of uploaded files labeled by upload directory.",
|
||||
"aptly_repos_package_count": "Current number of published packages labeled by source, distribution and component.",
|
||||
}
|
||||
|
||||
for _, metricFamily := range gathered {
|
||||
metricName := metricFamily.GetName()
|
||||
if expectedHelp, exists := expectedMetrics[metricName]; exists {
|
||||
c.Check(metricFamily.GetHelp(), Equals, expectedHelp,
|
||||
Commentf("Help text mismatch for metric: %s", metricName))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestCountPackagesByRepos(c *C) {
|
||||
// Test countPackagesByRepos function structure
|
||||
// Note: This function requires database context which we don't have in tests,
|
||||
// but we can test that it doesn't crash when called
|
||||
|
||||
// This will likely error due to no context, but should not panic
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
c.Fatalf("countPackagesByRepos panicked: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
countPackagesByRepos()
|
||||
|
||||
// If we get here, the function didn't panic
|
||||
c.Check(true, Equals, true)
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestGetBasePath(c *C) {
|
||||
// Test getBasePath function
|
||||
w := httptest.NewRecorder()
|
||||
ginCtx, _ := gin.CreateTestContext(w)
|
||||
|
||||
// Test with simple path (only returns first two segments)
|
||||
ginCtx.Request = httptest.NewRequest("GET", "/api/version", nil)
|
||||
basePath := getBasePath(ginCtx)
|
||||
c.Check(basePath, Equals, "/api/version")
|
||||
|
||||
// Test with path containing more segments (still returns first two)
|
||||
ginCtx.Request = httptest.NewRequest("GET", "/api/repos/test-repo", nil)
|
||||
basePath = getBasePath(ginCtx)
|
||||
c.Check(basePath, Equals, "/api/repos")
|
||||
|
||||
// Test with nested parameters (still returns first two)
|
||||
ginCtx.Request = httptest.NewRequest("GET", "/api/repos/repo1/packages", nil)
|
||||
basePath = getBasePath(ginCtx)
|
||||
c.Check(basePath, Equals, "/api/repos")
|
||||
|
||||
// Test with root path
|
||||
ginCtx.Request = httptest.NewRequest("GET", "/", nil)
|
||||
basePath = getBasePath(ginCtx)
|
||||
c.Check(basePath, Equals, "/")
|
||||
|
||||
// Test with single segment
|
||||
ginCtx.Request = httptest.NewRequest("GET", "/api", nil)
|
||||
basePath = getBasePath(ginCtx)
|
||||
c.Check(basePath, Equals, "/api")
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestGetURLSegment(c *C) {
|
||||
// Test getURLSegment function
|
||||
|
||||
// Test valid segments
|
||||
segment, err := getURLSegment("/api/repos/test", 0)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(*segment, Equals, "/api")
|
||||
|
||||
segment, err = getURLSegment("/api/repos/test", 1)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(*segment, Equals, "/repos")
|
||||
|
||||
segment, err = getURLSegment("/api/repos/test", 2)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(*segment, Equals, "/test")
|
||||
|
||||
// Test out of range
|
||||
_, err = getURLSegment("/api/repos", 3)
|
||||
c.Check(err, NotNil)
|
||||
|
||||
// Test root path
|
||||
segment, err = getURLSegment("/", 0)
|
||||
c.Check(err, NotNil) // No segments after removing empty string
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestInstrumentHandlerInFlight(c *C) {
|
||||
// Test instrumentHandlerInFlight middleware
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Create test gin context
|
||||
router := gin.New()
|
||||
|
||||
// Add instrumentation middleware
|
||||
router.Use(instrumentHandlerInFlight(apiRequestsInFlightGauge, getBasePath))
|
||||
|
||||
// Add test handler
|
||||
router.GET("/api/test", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
// Make request
|
||||
req := httptest.NewRequest("GET", "/api/test", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestInstrumentHandlerCounter(c *C) {
|
||||
// Test instrumentHandlerCounter middleware
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Create test gin context
|
||||
router := gin.New()
|
||||
|
||||
// Add instrumentation middleware
|
||||
router.Use(instrumentHandlerCounter(apiRequestsTotalCounter, getBasePath))
|
||||
|
||||
// Add test handler
|
||||
router.GET("/api/test", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
// Make request
|
||||
req := httptest.NewRequest("GET", "/api/test", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestInstrumentHandlerRequestSize(c *C) {
|
||||
// Test instrumentHandlerRequestSize middleware
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Create test gin context
|
||||
router := gin.New()
|
||||
|
||||
// Add instrumentation middleware
|
||||
router.Use(instrumentHandlerRequestSize(apiRequestSizeSummary, getBasePath))
|
||||
|
||||
// Add test handler
|
||||
router.POST("/api/test", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
// Make request with body
|
||||
req := httptest.NewRequest("POST", "/api/test", strings.NewReader("test body"))
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestInstrumentHandlerResponseSize(c *C) {
|
||||
// Test instrumentHandlerResponseSize middleware
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Create test gin context
|
||||
router := gin.New()
|
||||
|
||||
// Add instrumentation middleware
|
||||
router.Use(instrumentHandlerResponseSize(apiResponseSizeSummary, getBasePath))
|
||||
|
||||
// Add test handler
|
||||
router.GET("/api/test", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"data": strings.Repeat("x", 1000)})
|
||||
})
|
||||
|
||||
// Make request
|
||||
req := httptest.NewRequest("GET", "/api/test", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestInstrumentHandlerDuration(c *C) {
|
||||
// Test instrumentHandlerDuration middleware
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Create test gin context
|
||||
router := gin.New()
|
||||
|
||||
// Add instrumentation middleware
|
||||
router.Use(instrumentHandlerDuration(apiRequestsDurationSummary, getBasePath))
|
||||
|
||||
// Add test handler
|
||||
router.GET("/api/test", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
// Make request
|
||||
req := httptest.NewRequest("GET", "/api/test", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestMetricsRegistration(c *C) {
|
||||
// Test that metrics registration works correctly with gin router
|
||||
MetricsCollectorRegistrar.Register(s.router.(*gin.Engine))
|
||||
|
||||
// Create a test request to trigger middleware
|
||||
req, _ := http.NewRequest("GET", "/test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Add a test handler
|
||||
s.router.(*gin.Engine).GET("/test", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"test": "response"})
|
||||
})
|
||||
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
c.Check(MetricsCollectorRegistrar.hasRegistered, Equals, true)
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestMetricsErrorConditions(c *C) {
|
||||
// Test error handling in metrics collection
|
||||
|
||||
// Test with invalid label values (should not crash)
|
||||
invalidLabels := []string{"", "very_long_label_" + strings.Repeat("x", 1000), "label\nwith\nnewlines"}
|
||||
|
||||
for _, label := range invalidLabels {
|
||||
// These should not crash, even with invalid labels
|
||||
gauge := apiRequestsInFlightGauge.WithLabelValues("GET", label)
|
||||
gauge.Inc()
|
||||
gauge.Dec()
|
||||
|
||||
counter := apiRequestsTotalCounter.WithLabelValues("200", "GET", label)
|
||||
counter.Inc()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestMetricsValueRanges(c *C) {
|
||||
// Test metrics with various value ranges
|
||||
|
||||
// Test large values
|
||||
summary := apiRequestSizeSummary.WithLabelValues("200", "POST", "/api/large")
|
||||
summary.Observe(1e9) // 1GB
|
||||
summary.Observe(1e12) // 1TB
|
||||
|
||||
// Test very small values
|
||||
durationSummary := apiRequestsDurationSummary.WithLabelValues("200", "GET", "/api/fast")
|
||||
durationSummary.Observe(1e-9) // 1 nanosecond
|
||||
durationSummary.Observe(1e-6) // 1 microsecond
|
||||
|
||||
// Test zero values
|
||||
gauge := apiReposPackageCountGauge.WithLabelValues("empty", "dist", "comp")
|
||||
gauge.Set(0)
|
||||
|
||||
// Test negative values (should be handled gracefully)
|
||||
gauge.Set(-1) // May or may not be allowed by Prometheus, but shouldn't crash
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestMetricsWithSpecialCharacters(c *C) {
|
||||
// Test metrics with special characters in labels
|
||||
specialPaths := []string{
|
||||
"/api/repos/repo-with-dashes",
|
||||
"/api/repos/repo_with_underscores",
|
||||
"/api/repos/repo.with.dots",
|
||||
"/api/repos/repo+with+plus",
|
||||
"/api/repos/repo%20with%20encoded",
|
||||
}
|
||||
|
||||
for _, path := range specialPaths {
|
||||
counter := apiRequestsTotalCounter.WithLabelValues("200", "GET", path)
|
||||
counter.Inc()
|
||||
|
||||
gauge := apiRequestsInFlightGauge.WithLabelValues("GET", path)
|
||||
gauge.Inc()
|
||||
gauge.Dec()
|
||||
}
|
||||
}
|
||||
+12
-37
@@ -67,17 +67,17 @@ func (s *MiddlewareSuite) TestJSONMiddleware4xx(c *C) {
|
||||
outC := make(chan string)
|
||||
go func() {
|
||||
var buf bytes.Buffer
|
||||
_, _ = io.Copy(&buf, s.logReader)
|
||||
io.Copy(&buf, s.logReader)
|
||||
fmt.Println(buf.String())
|
||||
outC <- buf.String()
|
||||
}()
|
||||
|
||||
s.HTTPRequest(http.MethodGet, "/", nil)
|
||||
_ = s.logWriter.Close()
|
||||
s.logWriter.Close()
|
||||
capturedOutput := <-outC
|
||||
|
||||
var jsonMap map[string]interface{}
|
||||
_ = json.Unmarshal([]byte(capturedOutput), &jsonMap)
|
||||
json.Unmarshal([]byte(capturedOutput), &jsonMap)
|
||||
|
||||
if val, ok := jsonMap["level"]; ok {
|
||||
c.Check(val, Equals, "warn")
|
||||
@@ -130,17 +130,17 @@ func (s *MiddlewareSuite) TestJSONMiddleware2xx(c *C) {
|
||||
outC := make(chan string)
|
||||
go func() {
|
||||
var buf bytes.Buffer
|
||||
_, _ = io.Copy(&buf, s.logReader)
|
||||
io.Copy(&buf, s.logReader)
|
||||
fmt.Println(buf.String())
|
||||
outC <- buf.String()
|
||||
}()
|
||||
|
||||
s.HTTPRequest(http.MethodGet, "/api/healthy", nil)
|
||||
_ = s.logWriter.Close()
|
||||
s.logWriter.Close()
|
||||
capturedOutput := <-outC
|
||||
|
||||
var jsonMap map[string]interface{}
|
||||
_ = json.Unmarshal([]byte(capturedOutput), &jsonMap)
|
||||
json.Unmarshal([]byte(capturedOutput), &jsonMap)
|
||||
|
||||
if val, ok := jsonMap["level"]; ok {
|
||||
c.Check(val, Equals, "info")
|
||||
@@ -153,17 +153,17 @@ func (s *MiddlewareSuite) TestJSONMiddleware5xx(c *C) {
|
||||
outC := make(chan string)
|
||||
go func() {
|
||||
var buf bytes.Buffer
|
||||
_, _ = io.Copy(&buf, s.logReader)
|
||||
io.Copy(&buf, s.logReader)
|
||||
fmt.Println(buf.String())
|
||||
outC <- buf.String()
|
||||
}()
|
||||
|
||||
s.HTTPRequest(http.MethodGet, "/api/ready", nil)
|
||||
_ = s.logWriter.Close()
|
||||
s.logWriter.Close()
|
||||
capturedOutput := <-outC
|
||||
|
||||
var jsonMap map[string]interface{}
|
||||
_ = json.Unmarshal([]byte(capturedOutput), &jsonMap)
|
||||
json.Unmarshal([]byte(capturedOutput), &jsonMap)
|
||||
|
||||
if val, ok := jsonMap["level"]; ok {
|
||||
c.Check(val, Equals, "error")
|
||||
@@ -176,17 +176,17 @@ func (s *MiddlewareSuite) TestJSONMiddlewareRaw(c *C) {
|
||||
outC := make(chan string)
|
||||
go func() {
|
||||
var buf bytes.Buffer
|
||||
_, _ = io.Copy(&buf, s.logReader)
|
||||
io.Copy(&buf, s.logReader)
|
||||
fmt.Println(buf.String())
|
||||
outC <- buf.String()
|
||||
}()
|
||||
|
||||
s.HTTPRequest(http.MethodGet, "/api/healthy?test=raw", nil)
|
||||
_ = s.logWriter.Close()
|
||||
s.logWriter.Close()
|
||||
capturedOutput := <-outC
|
||||
|
||||
var jsonMap map[string]interface{}
|
||||
_ = json.Unmarshal([]byte(capturedOutput), &jsonMap)
|
||||
json.Unmarshal([]byte(capturedOutput), &jsonMap)
|
||||
|
||||
fmt.Println(capturedOutput)
|
||||
|
||||
@@ -253,28 +253,3 @@ func (s *MiddlewareSuite) TestGetURLSegment(c *C) {
|
||||
}
|
||||
c.Check(*segment, Equals, "/repos")
|
||||
}
|
||||
|
||||
func (s *MiddlewareSuite) TestInstrumentationMiddleware(c *C) {
|
||||
// Test instrumentation middleware functions
|
||||
router := gin.New()
|
||||
|
||||
// Add all instrumentation middleware
|
||||
router.Use(instrumentHandlerInFlight(apiRequestsInFlightGauge, getBasePath))
|
||||
router.Use(instrumentHandlerCounter(apiRequestsTotalCounter, getBasePath))
|
||||
router.Use(instrumentHandlerRequestSize(apiRequestSizeSummary, getBasePath))
|
||||
router.Use(instrumentHandlerResponseSize(apiResponseSizeSummary, getBasePath))
|
||||
router.Use(instrumentHandlerDuration(apiRequestsDurationSummary, getBasePath))
|
||||
|
||||
// Add test handler
|
||||
router.GET("/api/test", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
// Make request
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/api/test", nil)
|
||||
req.ContentLength = 42
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
}
|
||||
|
||||
+5
-5
@@ -43,7 +43,7 @@ func apiMirrorsList(c *gin.Context) {
|
||||
collection := collectionFactory.RemoteRepoCollection()
|
||||
|
||||
result := []*deb.RemoteRepo{}
|
||||
_ = collection.ForEach(func(repo *deb.RemoteRepo) error {
|
||||
collection.ForEach(func(repo *deb.RemoteRepo) error {
|
||||
result = append(result, repo)
|
||||
return nil
|
||||
})
|
||||
@@ -319,7 +319,7 @@ func apiMirrorsPackages(c *gin.Context) {
|
||||
}
|
||||
|
||||
if c.Request.URL.Query().Get("format") == "details" {
|
||||
_ = list.ForEach(func(p *deb.Package) error {
|
||||
list.ForEach(func(p *deb.Package) error {
|
||||
result = append(result, p)
|
||||
return nil
|
||||
})
|
||||
@@ -491,7 +491,7 @@ func apiMirrorsUpdate(c *gin.Context) {
|
||||
e := context.ReOpenDatabase()
|
||||
if e == nil {
|
||||
remote.MarkAsIdle()
|
||||
_ = collection.Update(remote)
|
||||
collection.Update(remote)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -579,7 +579,7 @@ func apiMirrorsUpdate(c *gin.Context) {
|
||||
file, e = os.CreateTemp("", task.File.Filename)
|
||||
if e == nil {
|
||||
task.TempDownPath = file.Name()
|
||||
_ = file.Close()
|
||||
file.Close()
|
||||
}
|
||||
}
|
||||
if e != nil {
|
||||
@@ -653,7 +653,7 @@ func apiMirrorsUpdate(c *gin.Context) {
|
||||
}
|
||||
|
||||
log.Info().Msgf("%s: Finalizing download...", b.Name)
|
||||
_ = remote.FinalizeDownload(collectionFactory, out)
|
||||
remote.FinalizeDownload(collectionFactory, out)
|
||||
err = collectionFactory.RemoteRepoCollection().Update(remote)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
|
||||
+1
-28
@@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
type MirrorSuite struct {
|
||||
APISuite
|
||||
ApiSuite
|
||||
}
|
||||
|
||||
var _ = Suite(&MirrorSuite{})
|
||||
@@ -38,30 +38,3 @@ func (s *MirrorSuite) TestCreateMirror(c *C) {
|
||||
c.Check(response.Code, Equals, 400)
|
||||
c.Check(response.Body.String(), Equals, "")
|
||||
}
|
||||
|
||||
func (s *MirrorSuite) TestMirrorShow(c *C) {
|
||||
// Test showing a specific mirror
|
||||
response, _ := s.HTTPRequest("GET", "/api/mirrors/test-mirror", nil)
|
||||
c.Check(response.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *MirrorSuite) TestMirrorUpdate(c *C) {
|
||||
// Test updating a mirror
|
||||
body, _ := json.Marshal(gin.H{
|
||||
"ArchiveURL": "http://new.archive.url/debian",
|
||||
})
|
||||
response, _ := s.HTTPRequest("PUT", "/api/mirrors/test-mirror", bytes.NewReader(body))
|
||||
c.Check(response.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *MirrorSuite) TestMirrorPackages(c *C) {
|
||||
// Test listing packages in a mirror
|
||||
response, _ := s.HTTPRequest("GET", "/api/mirrors/test-mirror/packages", nil)
|
||||
c.Check(response.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *MirrorSuite) TestMirrorUpdateRun(c *C) {
|
||||
// Test running mirror update
|
||||
response, _ := s.HTTPRequest("PUT", "/api/mirrors/test-mirror/update", nil)
|
||||
c.Check(response.Code, Equals, 404)
|
||||
}
|
||||
|
||||
+1
-35
@@ -1,52 +1,18 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type PackagesSuite struct {
|
||||
APISuite
|
||||
ApiSuite
|
||||
}
|
||||
|
||||
var _ = Suite(&PackagesSuite{})
|
||||
|
||||
func (s *PackagesSuite) TestPackageShow(c *C) {
|
||||
// Test showing a specific package
|
||||
response, _ := s.HTTPRequest("GET", "/api/packages/Pamd64%20test%201.0%20abc123", nil)
|
||||
// Will return 404 as the package doesn't exist
|
||||
c.Check(response.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *PackagesSuite) TestPackagesList(c *C) {
|
||||
// Test listing all packages
|
||||
response, _ := s.HTTPRequest("GET", "/api/packages", nil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
|
||||
var result []interface{}
|
||||
err := json.Unmarshal(response.Body.Bytes(), &result)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(result, NotNil)
|
||||
}
|
||||
|
||||
func (s *PackagesSuite) TestPackagesGetMaximumVersion(c *C) {
|
||||
// Create dummy repo first
|
||||
body, _ := json.Marshal(gin.H{"Name": "dummy"})
|
||||
resp, err := s.HTTPRequest("POST", "/api/repos", bytes.NewReader(body))
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(resp.Code, Equals, 201)
|
||||
|
||||
// Now test packages with maximumVersion
|
||||
response, err := s.HTTPRequest("GET", "/api/repos/dummy/packages?maximumVersion=1", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
c.Check(response.Body.String(), Equals, "[]")
|
||||
|
||||
// Clean up
|
||||
resp, err = s.HTTPRequest("DELETE", "/api/repos/dummy?force=1", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(resp.Code, Equals, 200)
|
||||
}
|
||||
|
||||
+15
-36
@@ -267,7 +267,13 @@ func apiPublishRepoOrSnapshot(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
err = CheckGroup(c, localRepo.LdapGroup)
|
||||
if err != nil {
|
||||
c.AbortWithError(403, err)
|
||||
}
|
||||
|
||||
resources = append(resources, string(localRepo.Key()))
|
||||
|
||||
sources = append(sources, localRepo)
|
||||
}
|
||||
} else {
|
||||
@@ -343,7 +349,7 @@ func apiPublishRepoOrSnapshot(c *gin.Context) {
|
||||
|
||||
duplicate := collection.CheckDuplicate(published)
|
||||
if duplicate != nil {
|
||||
_ = collectionFactory.PublishedRepoCollection().LoadComplete(duplicate, collectionFactory)
|
||||
collectionFactory.PublishedRepoCollection().LoadComplete(duplicate, collectionFactory)
|
||||
return &task.ProcessReturnValue{Code: http.StatusBadRequest, Value: nil}, fmt.Errorf("prefix/distribution already used by another published repo: %s", duplicate)
|
||||
}
|
||||
|
||||
@@ -378,13 +384,6 @@ type publishedRepoUpdateSwitchParams struct {
|
||||
AcquireByHash *bool ` json:"AcquireByHash" example:"false"`
|
||||
// Enable multiple packages with the same filename in different distributions
|
||||
MultiDist *bool ` json:"MultiDist" example:"false"`
|
||||
// Metadata fields (optional) - if provided, will update the published repository metadata
|
||||
Origin *string ` json:"Origin,omitempty"`
|
||||
Label *string ` json:"Label,omitempty"`
|
||||
Suite *string ` json:"Suite,omitempty"`
|
||||
Codename *string ` json:"Codename,omitempty"`
|
||||
NotAutomatic *string ` json:"NotAutomatic,omitempty"`
|
||||
ButAutomaticUpgrades *string ` json:"ButAutomaticUpgrades,omitempty"`
|
||||
}
|
||||
|
||||
// @Summary Update Published Repository
|
||||
@@ -473,32 +472,12 @@ func apiPublishUpdateSwitch(c *gin.Context) {
|
||||
published.MultiDist = *b.MultiDist
|
||||
}
|
||||
|
||||
// Update metadata fields if provided
|
||||
if b.Origin != nil {
|
||||
published.Origin = *b.Origin
|
||||
}
|
||||
if b.Label != nil {
|
||||
published.Label = *b.Label
|
||||
}
|
||||
if b.Suite != nil {
|
||||
published.Suite = *b.Suite
|
||||
}
|
||||
if b.Codename != nil {
|
||||
published.Codename = *b.Codename
|
||||
}
|
||||
if b.NotAutomatic != nil {
|
||||
published.NotAutomatic = *b.NotAutomatic
|
||||
}
|
||||
if b.ButAutomaticUpgrades != nil {
|
||||
published.ButAutomaticUpgrades = *b.ButAutomaticUpgrades
|
||||
}
|
||||
|
||||
resources := []string{string(published.Key())}
|
||||
taskName := fmt.Sprintf("Update published %s repository %s/%s", published.SourceKind, published.StoragePrefix(), published.Distribution)
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(out aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err = collection.LoadComplete(published, collectionFactory)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("Unable to update: %s", err)
|
||||
}
|
||||
|
||||
revision := published.ObtainRevision()
|
||||
@@ -514,12 +493,12 @@ func apiPublishUpdateSwitch(c *gin.Context) {
|
||||
|
||||
result, err := published.Update(collectionFactory, out)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("Unable to update: %s", err)
|
||||
}
|
||||
|
||||
err = published.Publish(context.PackagePool(), context, collectionFactory, signer, out, b.ForceOverwrite, context.SkelPath())
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("Unable to update: %s", err)
|
||||
}
|
||||
|
||||
err = collection.Update(published)
|
||||
@@ -652,7 +631,7 @@ func apiPublishAddSource(c *gin.Context) {
|
||||
|
||||
resources := []string{string(published.Key())}
|
||||
taskName := fmt.Sprintf("Update published %s repository %s/%s", published.SourceKind, published.StoragePrefix(), published.Distribution)
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(out aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err = collection.Update(published)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to save to DB: %s", err)
|
||||
@@ -766,7 +745,7 @@ func apiPublishSetSources(c *gin.Context) {
|
||||
|
||||
resources := []string{string(published.Key())}
|
||||
taskName := fmt.Sprintf("Update published %s repository %s/%s", published.SourceKind, published.StoragePrefix(), published.Distribution)
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(out aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err = collection.Update(published)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to save to DB: %s", err)
|
||||
@@ -816,7 +795,7 @@ func apiPublishDropChanges(c *gin.Context) {
|
||||
|
||||
resources := []string{string(published.Key())}
|
||||
taskName := fmt.Sprintf("Update published %s repository %s/%s", published.SourceKind, published.StoragePrefix(), published.Distribution)
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(out aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err = collection.Update(published)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to save to DB: %s", err)
|
||||
@@ -896,7 +875,7 @@ func apiPublishUpdateSource(c *gin.Context) {
|
||||
|
||||
resources := []string{string(published.Key())}
|
||||
taskName := fmt.Sprintf("Update published %s repository %s/%s", published.SourceKind, published.StoragePrefix(), published.Distribution)
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(out aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err = collection.Update(published)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to save to DB: %s", err)
|
||||
@@ -959,7 +938,7 @@ func apiPublishRemoveSource(c *gin.Context) {
|
||||
|
||||
resources := []string{string(published.Key())}
|
||||
taskName := fmt.Sprintf("Update published %s repository %s/%s", published.SourceKind, published.StoragePrefix(), published.Distribution)
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(out aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err = collection.Update(published)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to save to DB: %s", err)
|
||||
|
||||
@@ -1,675 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type PublishAPITestSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&PublishAPITestSuite{})
|
||||
|
||||
func (s *PublishAPITestSuite) SetUpTest(c *C) {
|
||||
s.APISuite.SetUpTest(c)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestPublishList(c *C) {
|
||||
// Test listing published repositories
|
||||
req, _ := http.NewRequest("GET", "/api/publish", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
c.Check(w.Header().Get("Content-Type"), Equals, "application/json; charset=utf-8")
|
||||
|
||||
var result []*deb.PublishedRepo
|
||||
err := json.Unmarshal(w.Body.Bytes(), &result)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(result, NotNil)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestPublishShow(c *C) {
|
||||
// Test showing a specific published repository
|
||||
// First, we need to create a snapshot and publish it
|
||||
// For now, test the endpoint structure
|
||||
req, _ := http.NewRequest("GET", "/api/publish/test/bookworm", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return 404 as the publish doesn't exist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestPublishUpdate(c *C) {
|
||||
// Test updating a published repository
|
||||
params := struct {
|
||||
Signing signingParams `json:"Signing"`
|
||||
}{
|
||||
Signing: signingParams{Skip: true},
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("PUT", "/api/publish/test/bookworm", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return 404 as the publish doesn't exist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestPublishDrop(c *C) {
|
||||
// Test dropping a published repository
|
||||
req, _ := http.NewRequest("DELETE", "/api/publish/test/bookworm", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return 404 as the publish doesn't exist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestPublishListChanges(c *C) {
|
||||
// Test listing changes in a published repository
|
||||
req, _ := http.NewRequest("GET", "/api/publish/test/bookworm/sources", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return 404 as the publish doesn't exist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestPublishAddSource(c *C) {
|
||||
// Test adding a source to published repository
|
||||
params := sourceParams{
|
||||
Component: "contrib",
|
||||
Name: "test-snap2",
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/publish/test/bookworm/sources", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return 404 as the publish doesn't exist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestPublishUpdateSource(c *C) {
|
||||
// Test updating a source in published repository
|
||||
params := sourceParams{
|
||||
Component: "main",
|
||||
Name: "updated-snap",
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("PUT", "/api/publish/test/bookworm/sources/main", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return 404 as the publish doesn't exist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestPublishRemoveSource(c *C) {
|
||||
// Test removing a source from published repository
|
||||
req, _ := http.NewRequest("DELETE", "/api/publish/test/bookworm/sources/contrib", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return 404 as the publish doesn't exist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestPublishSetSources(c *C) {
|
||||
// Test setting sources for published repository
|
||||
params := struct {
|
||||
Sources []sourceParams `json:"Sources"`
|
||||
}{
|
||||
Sources: []sourceParams{
|
||||
{Component: "main", Name: "snap1"},
|
||||
{Component: "contrib", Name: "snap2"},
|
||||
},
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("PUT", "/api/publish/test/bookworm/sources", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return 404 as the publish doesn't exist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestPublishDropChanges(c *C) {
|
||||
// Test dropping changes from published repository
|
||||
req, _ := http.NewRequest("DELETE", "/api/publish/test/bookworm/sources", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return 404 as the publish doesn't exist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestGetSigner(c *C) {
|
||||
// Test getSigner function
|
||||
// Test with Skip = true
|
||||
skipParams := &signingParams{Skip: true}
|
||||
signer, err := getSigner(skipParams)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(signer, IsNil) // Should return nil when Skip is true
|
||||
|
||||
// Test with Skip = false - will use context signer
|
||||
params := &signingParams{
|
||||
Skip: false,
|
||||
GpgKey: "A0546A43624A8331",
|
||||
Keyring: "trustedkeys.gpg",
|
||||
SecretKeyring: "secretkeys.gpg",
|
||||
Passphrase: "test",
|
||||
PassphraseFile: "/tmp/passphrase",
|
||||
}
|
||||
signer, err = getSigner(params)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(signer, NotNil)
|
||||
}
|
||||
|
||||
|
||||
func (s *PublishAPITestSuite) TestSigningParamsStruct(c *C) {
|
||||
// Test signingParams struct and JSON marshaling/unmarshaling
|
||||
params := signingParams{
|
||||
Skip: true,
|
||||
GpgKey: "A0546A43624A8331",
|
||||
Keyring: "trustedkeys.gpg",
|
||||
SecretKeyring: "secretkeys.gpg",
|
||||
Passphrase: "verysecure",
|
||||
PassphraseFile: "/etc/aptly.passphrase",
|
||||
}
|
||||
|
||||
// Test JSON marshaling
|
||||
jsonData, err := json.Marshal(params)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(string(jsonData), Matches, ".*Skip.*true.*")
|
||||
c.Check(string(jsonData), Matches, ".*GpgKey.*A0546A43624A8331.*")
|
||||
|
||||
// Test JSON unmarshaling
|
||||
var unmarshaled signingParams
|
||||
err = json.Unmarshal(jsonData, &unmarshaled)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(unmarshaled.Skip, Equals, true)
|
||||
c.Check(unmarshaled.GpgKey, Equals, "A0546A43624A8331")
|
||||
c.Check(unmarshaled.Keyring, Equals, "trustedkeys.gpg")
|
||||
c.Check(unmarshaled.SecretKeyring, Equals, "secretkeys.gpg")
|
||||
c.Check(unmarshaled.Passphrase, Equals, "verysecure")
|
||||
c.Check(unmarshaled.PassphraseFile, Equals, "/etc/aptly.passphrase")
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestSourceParamsStruct(c *C) {
|
||||
// Test sourceParams struct and JSON marshaling/unmarshaling
|
||||
params := sourceParams{
|
||||
Component: "main",
|
||||
Name: "snap1",
|
||||
}
|
||||
|
||||
// Test JSON marshaling
|
||||
jsonData, err := json.Marshal(params)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(string(jsonData), Matches, ".*Component.*main.*")
|
||||
c.Check(string(jsonData), Matches, ".*Name.*snap1.*")
|
||||
|
||||
// Test JSON unmarshaling
|
||||
var unmarshaled sourceParams
|
||||
err = json.Unmarshal(jsonData, &unmarshaled)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(unmarshaled.Component, Equals, "main")
|
||||
c.Check(unmarshaled.Name, Equals, "snap1")
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestGetSignerSkip(c *C) {
|
||||
// Test getSigner with Skip=true
|
||||
options := &signingParams{
|
||||
Skip: true,
|
||||
}
|
||||
|
||||
signer, err := getSigner(options)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(signer, IsNil)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestSlashEscape(c *C) {
|
||||
// Test slashEscape function
|
||||
testCases := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"", "."},
|
||||
{"test_path", "test/path"},
|
||||
{"test__path", "test_path"},
|
||||
{"test_path_file", "test/path/file"},
|
||||
{"test__test__test", "test_test_test"},
|
||||
{"_test_", "test/"},
|
||||
{"__", "_"},
|
||||
{"test_path__with__underscores", "test/path_with_underscores"},
|
||||
{"complex_path__example_test", "complex/path_example/test"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
result := slashEscape(tc.input)
|
||||
c.Check(result, Equals, tc.expected, Commentf("Input: %s", tc.input))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestSlashEscapeEdgeCases(c *C) {
|
||||
// Test edge cases for slashEscape
|
||||
edgeCases := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"simple", "simple"},
|
||||
{"no_underscores_here", "no/underscores/here"},
|
||||
{"double__only", "double_only"},
|
||||
{"_", "."},
|
||||
{"__only", "_only"},
|
||||
{"only_", "only/"},
|
||||
{"mixed_case__Test_Path", "mixed/case_Test/Path"},
|
||||
{"numbers_123__test", "numbers/123_test"},
|
||||
{"special-chars.test_path", "special-chars.test/path"},
|
||||
}
|
||||
|
||||
for _, tc := range edgeCases {
|
||||
result := slashEscape(tc.input)
|
||||
c.Check(result, Equals, tc.expected, Commentf("Input: '%s'", tc.input))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestApiPublishListBasic(c *C) {
|
||||
// Test basic API publish list endpoint
|
||||
req, _ := http.NewRequest("GET", "/api/publish", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Now context is set up properly through APISuite
|
||||
s.router.ServeHTTP(w, req)
|
||||
// Should return OK with empty list
|
||||
c.Check(w.Code, Equals, http.StatusOK)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestApiPublishShowBasic(c *C) {
|
||||
// Test basic API publish show endpoint
|
||||
req, _ := http.NewRequest("GET", "/api/publish/test-prefix/test-dist", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// This will fail because context is not set up properly
|
||||
s.router.ServeHTTP(w, req)
|
||||
// Expect some kind of error due to missing context
|
||||
c.Check(w.Code, Not(Equals), http.StatusOK)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestApiPublishShowWithSlashEscape(c *C) {
|
||||
// Test API publish show with slash escape characters
|
||||
req, _ := http.NewRequest("GET", "/api/publish/test__prefix/test_dist", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
s.router.ServeHTTP(w, req)
|
||||
// Should attempt to process the escaped path
|
||||
c.Check(w.Code, Not(Equals), http.StatusOK) // Expected to fail due to missing context
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestPublishedRepoCreateParamsStruct(c *C) {
|
||||
// Test publishedRepoCreateParams struct
|
||||
skipContents := true
|
||||
skipCleanup := false
|
||||
skipBz2 := true
|
||||
acquireByHash := false
|
||||
multiDist := true
|
||||
|
||||
params := publishedRepoCreateParams{
|
||||
SourceKind: "snapshot",
|
||||
Sources: []sourceParams{{Component: "main", Name: "test-snap"}},
|
||||
Distribution: "bookworm",
|
||||
Label: "Test Label",
|
||||
Origin: "Test Origin",
|
||||
ForceOverwrite: true,
|
||||
Architectures: []string{"amd64", "armhf"},
|
||||
Signing: signingParams{
|
||||
Skip: false,
|
||||
GpgKey: "A0546A43624A8331",
|
||||
},
|
||||
NotAutomatic: "yes",
|
||||
ButAutomaticUpgrades: "yes",
|
||||
SkipContents: &skipContents,
|
||||
SkipCleanup: &skipCleanup,
|
||||
SkipBz2: &skipBz2,
|
||||
AcquireByHash: &acquireByHash,
|
||||
MultiDist: &multiDist,
|
||||
}
|
||||
|
||||
// Test JSON marshaling
|
||||
jsonData, err := json.Marshal(params)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(string(jsonData), Matches, ".*SourceKind.*snapshot.*")
|
||||
c.Check(string(jsonData), Matches, ".*Distribution.*bookworm.*")
|
||||
c.Check(string(jsonData), Matches, ".*Label.*Test Label.*")
|
||||
c.Check(string(jsonData), Matches, ".*Origin.*Test Origin.*")
|
||||
c.Check(string(jsonData), Matches, ".*ForceOverwrite.*true.*")
|
||||
|
||||
// Test JSON unmarshaling
|
||||
var unmarshaled publishedRepoCreateParams
|
||||
err = json.Unmarshal(jsonData, &unmarshaled)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(unmarshaled.SourceKind, Equals, "snapshot")
|
||||
c.Check(unmarshaled.Distribution, Equals, "bookworm")
|
||||
c.Check(unmarshaled.Label, Equals, "Test Label")
|
||||
c.Check(unmarshaled.Origin, Equals, "Test Origin")
|
||||
c.Check(unmarshaled.ForceOverwrite, Equals, true)
|
||||
c.Check(len(unmarshaled.Sources), Equals, 1)
|
||||
c.Check(unmarshaled.Sources[0].Component, Equals, "main")
|
||||
c.Check(unmarshaled.Sources[0].Name, Equals, "test-snap")
|
||||
c.Check(len(unmarshaled.Architectures), Equals, 2)
|
||||
c.Check(unmarshaled.Architectures[0], Equals, "amd64")
|
||||
c.Check(unmarshaled.Architectures[1], Equals, "armhf")
|
||||
c.Check(*unmarshaled.SkipContents, Equals, true)
|
||||
c.Check(*unmarshaled.SkipCleanup, Equals, false)
|
||||
c.Check(*unmarshaled.SkipBz2, Equals, true)
|
||||
c.Check(*unmarshaled.AcquireByHash, Equals, false)
|
||||
c.Check(*unmarshaled.MultiDist, Equals, true)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestPublishedRepoUpdateSwitchParamsStruct(c *C) {
|
||||
// Test publishedRepoUpdateSwitchParams struct
|
||||
skipContents := false
|
||||
skipBz2 := true
|
||||
skipCleanup := true
|
||||
acquireByHash := true
|
||||
multiDist := false
|
||||
|
||||
params := publishedRepoUpdateSwitchParams{
|
||||
ForceOverwrite: true,
|
||||
Signing: signingParams{
|
||||
Skip: true,
|
||||
GpgKey: "testkey",
|
||||
Keyring: "test.gpg",
|
||||
},
|
||||
SkipContents: &skipContents,
|
||||
SkipBz2: &skipBz2,
|
||||
SkipCleanup: &skipCleanup,
|
||||
Snapshots: []sourceParams{{Component: "main", Name: "snap1"}, {Component: "contrib", Name: "snap2"}},
|
||||
AcquireByHash: &acquireByHash,
|
||||
MultiDist: &multiDist,
|
||||
}
|
||||
|
||||
// Test JSON marshaling
|
||||
jsonData, err := json.Marshal(params)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(string(jsonData), Matches, ".*ForceOverwrite.*true.*")
|
||||
c.Check(string(jsonData), Matches, ".*SkipContents.*false.*")
|
||||
c.Check(string(jsonData), Matches, ".*SkipBz2.*true.*")
|
||||
|
||||
// Test JSON unmarshaling
|
||||
var unmarshaled publishedRepoUpdateSwitchParams
|
||||
err = json.Unmarshal(jsonData, &unmarshaled)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(unmarshaled.ForceOverwrite, Equals, true)
|
||||
c.Check(unmarshaled.Signing.Skip, Equals, true)
|
||||
c.Check(unmarshaled.Signing.GpgKey, Equals, "testkey")
|
||||
c.Check(unmarshaled.Signing.Keyring, Equals, "test.gpg")
|
||||
c.Check(*unmarshaled.SkipContents, Equals, false)
|
||||
c.Check(*unmarshaled.SkipBz2, Equals, true)
|
||||
c.Check(*unmarshaled.SkipCleanup, Equals, true)
|
||||
c.Check(*unmarshaled.AcquireByHash, Equals, true)
|
||||
c.Check(*unmarshaled.MultiDist, Equals, false)
|
||||
c.Check(len(unmarshaled.Snapshots), Equals, 2)
|
||||
c.Check(unmarshaled.Snapshots[0].Component, Equals, "main")
|
||||
c.Check(unmarshaled.Snapshots[0].Name, Equals, "snap1")
|
||||
c.Check(unmarshaled.Snapshots[1].Component, Equals, "contrib")
|
||||
c.Check(unmarshaled.Snapshots[1].Name, Equals, "snap2")
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestApiPublishRepoOrSnapshotInvalidJSON(c *C) {
|
||||
// Test POST endpoint with invalid JSON
|
||||
req, _ := http.NewRequest("POST", "/api/publish/test-prefix", strings.NewReader("invalid json"))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestApiPublishRepoOrSnapshotEmptySources(c *C) {
|
||||
// Test POST endpoint with empty sources
|
||||
params := publishedRepoCreateParams{
|
||||
SourceKind: "snapshot",
|
||||
Sources: []sourceParams{}, // Empty sources
|
||||
Distribution: "test",
|
||||
}
|
||||
|
||||
jsonData, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/publish/test-prefix", bytes.NewReader(jsonData))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should return 400 due to empty sources
|
||||
c.Check(w.Code, Equals, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestApiPublishRepoOrSnapshotUnknownSourceKind(c *C) {
|
||||
// Test POST endpoint with unknown source kind
|
||||
params := publishedRepoCreateParams{
|
||||
SourceKind: "unknown",
|
||||
Sources: []sourceParams{{Component: "main", Name: "test"}},
|
||||
Distribution: "test",
|
||||
}
|
||||
|
||||
jsonData, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/publish/test-prefix", bytes.NewReader(jsonData))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should return 400 due to unknown source kind
|
||||
c.Check(w.Code, Equals, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestApiPublishRepoOrSnapshotValidRequest(c *C) {
|
||||
// Test POST endpoint with valid request (will fail due to missing context)
|
||||
params := publishedRepoCreateParams{
|
||||
SourceKind: deb.SourceSnapshot,
|
||||
Sources: []sourceParams{{Component: "main", Name: "test-snap"}},
|
||||
Distribution: "test-dist",
|
||||
Signing: signingParams{Skip: true},
|
||||
}
|
||||
|
||||
jsonData, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/publish/test-prefix", bytes.NewReader(jsonData))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will fail due to missing context but should get past basic validation
|
||||
c.Check(w.Code, Not(Equals), http.StatusBadRequest) // Should not be a 400 error
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestApiPublishRepoOrSnapshotLocalRepoSourceKind(c *C) {
|
||||
// Test POST endpoint with local repo source kind
|
||||
params := publishedRepoCreateParams{
|
||||
SourceKind: deb.SourceLocalRepo,
|
||||
Sources: []sourceParams{{Component: "main", Name: "test-repo"}},
|
||||
Distribution: "test-dist",
|
||||
Signing: signingParams{Skip: true},
|
||||
}
|
||||
|
||||
jsonData, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/publish/test-prefix", bytes.NewReader(jsonData))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will fail due to missing context but should get past basic validation
|
||||
c.Check(w.Code, Not(Equals), http.StatusBadRequest) // Should not be a 400 error
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestSigningParamsEdgeCases(c *C) {
|
||||
// Test signingParams with edge cases
|
||||
testCases := []signingParams{
|
||||
{Skip: true}, // Minimal case
|
||||
{Skip: false, GpgKey: "", Keyring: "", SecretKeyring: "", Passphrase: "", PassphraseFile: ""}, // Empty strings
|
||||
{Skip: false, GpgKey: "very-long-key-id-123456789012345678901234567890", Keyring: "very-long-keyring-name.gpg"}, // Long values
|
||||
{Skip: false, Passphrase: "password with spaces and special chars !@#$%^&*()"}, // Special characters
|
||||
{Skip: false, PassphraseFile: "/very/long/path/to/passphrase/file/that/might/not/exist.txt"}, // Long file path
|
||||
}
|
||||
|
||||
for i, params := range testCases {
|
||||
// Test JSON marshaling/unmarshaling
|
||||
jsonData, err := json.Marshal(params)
|
||||
c.Check(err, IsNil, Commentf("Test case %d", i))
|
||||
|
||||
var unmarshaled signingParams
|
||||
err = json.Unmarshal(jsonData, &unmarshaled)
|
||||
c.Check(err, IsNil, Commentf("Test case %d", i))
|
||||
c.Check(unmarshaled.Skip, Equals, params.Skip, Commentf("Test case %d", i))
|
||||
c.Check(unmarshaled.GpgKey, Equals, params.GpgKey, Commentf("Test case %d", i))
|
||||
c.Check(unmarshaled.Keyring, Equals, params.Keyring, Commentf("Test case %d", i))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestSourceParamsEdgeCases(c *C) {
|
||||
// Test sourceParams with edge cases
|
||||
testCases := []sourceParams{
|
||||
{Component: "", Name: ""}, // Empty strings
|
||||
{Component: "very-long-component-name-with-dashes-and-numbers-123", Name: "very-long-name-456"}, // Long values
|
||||
{Component: "comp.with.dots", Name: "name_with_underscores"}, // Special characters
|
||||
{Component: "UPPERCASE", Name: "MixedCase"}, // Case variations
|
||||
{Component: "123numeric", Name: "456numbers"}, // Numeric values
|
||||
}
|
||||
|
||||
for i, params := range testCases {
|
||||
// Test JSON marshaling/unmarshaling
|
||||
jsonData, err := json.Marshal(params)
|
||||
c.Check(err, IsNil, Commentf("Test case %d", i))
|
||||
|
||||
var unmarshaled sourceParams
|
||||
err = json.Unmarshal(jsonData, &unmarshaled)
|
||||
c.Check(err, IsNil, Commentf("Test case %d", i))
|
||||
c.Check(unmarshaled.Component, Equals, params.Component, Commentf("Test case %d", i))
|
||||
c.Check(unmarshaled.Name, Equals, params.Name, Commentf("Test case %d", i))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestSlashEscapeComprehensive(c *C) {
|
||||
// Comprehensive test of slashEscape function
|
||||
testCases := []struct {
|
||||
input string
|
||||
expected string
|
||||
description string
|
||||
}{
|
||||
{"", ".", "empty string"},
|
||||
{"simple", "simple", "no underscores"},
|
||||
{"one_underscore", "one/underscore", "single underscore"},
|
||||
{"two__underscores", "two_underscores", "double underscore"},
|
||||
{"_leading", "leading", "leading underscore"},
|
||||
{"trailing_", "trailing/", "trailing underscore"},
|
||||
{"_both_", "both/", "both leading and trailing"},
|
||||
{"__double_leading", "_double/leading", "double leading underscore"},
|
||||
{"trailing_double__", "trailing/double_", "double trailing underscore"},
|
||||
{"mixed_single__double_combo", "mixed/single_double/combo", "mixed single and double"},
|
||||
{"complex_path__with_multiple__sections", "complex/path_with/multiple_sections", "complex path"},
|
||||
{"a_b_c_d_e", "a/b/c/d/e", "multiple single underscores"},
|
||||
{"a__b__c__d__e", "a_b_c_d_e", "multiple double underscores"},
|
||||
{"_a__b_c__d_", "a_b/c_d/", "mixed pattern"},
|
||||
{"test___triple", "test_/triple", "triple underscore"},
|
||||
{"test____quad", "test__quad", "quadruple underscore"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
result := slashEscape(tc.input)
|
||||
c.Check(result, Equals, tc.expected, Commentf("Test case: %s (input: '%s')", tc.description, tc.input))
|
||||
}
|
||||
}
|
||||
|
||||
// Mock implementations for testing context dependencies
|
||||
type MockSigner struct {
|
||||
initError error
|
||||
key string
|
||||
keyring string
|
||||
secretKeyring string
|
||||
passphrase string
|
||||
passphraseFile string
|
||||
batch bool
|
||||
}
|
||||
|
||||
func (m *MockSigner) SetKey(key string) { m.key = key }
|
||||
func (m *MockSigner) SetKeyRing(keyring, secretKeyring string) {
|
||||
m.keyring = keyring
|
||||
m.secretKeyring = secretKeyring
|
||||
}
|
||||
func (m *MockSigner) SetPassphrase(passphrase, passphraseFile string) {
|
||||
m.passphrase = passphrase
|
||||
m.passphraseFile = passphraseFile
|
||||
}
|
||||
func (m *MockSigner) SetBatch(batch bool) { m.batch = batch }
|
||||
func (m *MockSigner) Init() error { return m.initError }
|
||||
|
||||
func (s *PublishAPITestSuite) TestGetSignerMockSuccess(c *C) {
|
||||
// Test getSigner logic with mock (can't test actual getSigner due to context dependencies)
|
||||
options := &signingParams{
|
||||
Skip: false,
|
||||
GpgKey: "testkey",
|
||||
Keyring: "test.gpg",
|
||||
SecretKeyring: "secret.gpg",
|
||||
Passphrase: "testpass",
|
||||
PassphraseFile: "/tmp/passfile",
|
||||
}
|
||||
|
||||
// Mock the signer behavior
|
||||
mockSigner := &MockSigner{initError: nil}
|
||||
|
||||
// Simulate what getSigner would do
|
||||
mockSigner.SetKey(options.GpgKey)
|
||||
mockSigner.SetKeyRing(options.Keyring, options.SecretKeyring)
|
||||
mockSigner.SetPassphrase(options.Passphrase, options.PassphraseFile)
|
||||
mockSigner.SetBatch(true)
|
||||
err := mockSigner.Init()
|
||||
|
||||
c.Check(err, IsNil)
|
||||
c.Check(mockSigner.key, Equals, "testkey")
|
||||
c.Check(mockSigner.keyring, Equals, "test.gpg")
|
||||
c.Check(mockSigner.secretKeyring, Equals, "secret.gpg")
|
||||
c.Check(mockSigner.passphrase, Equals, "testpass")
|
||||
c.Check(mockSigner.passphraseFile, Equals, "/tmp/passfile")
|
||||
c.Check(mockSigner.batch, Equals, true)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestGetSignerMockError(c *C) {
|
||||
// Test getSigner logic with mock error
|
||||
options := &signingParams{
|
||||
Skip: false,
|
||||
GpgKey: "invalidkey",
|
||||
}
|
||||
|
||||
// Mock the signer behavior with error
|
||||
mockSigner := &MockSigner{initError: fmt.Errorf("mock init error")}
|
||||
|
||||
mockSigner.SetKey(options.GpgKey)
|
||||
mockSigner.SetKeyRing(options.Keyring, options.SecretKeyring)
|
||||
mockSigner.SetPassphrase(options.Passphrase, options.PassphraseFile)
|
||||
mockSigner.SetBatch(true)
|
||||
err := mockSigner.Init()
|
||||
|
||||
c.Check(err, NotNil)
|
||||
c.Check(err.Error(), Equals, "mock init error")
|
||||
}
|
||||
+67
-38
@@ -29,14 +29,14 @@ func reposListInAPIMode(localRepos map[string]utils.FileSystemPublishRoot) gin.H
|
||||
return func(c *gin.Context) {
|
||||
c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
c.Writer.Flush()
|
||||
_, _ = c.Writer.WriteString("<pre>\n")
|
||||
c.Writer.WriteString("<pre>\n")
|
||||
if len(localRepos) == 0 {
|
||||
_, _ = c.Writer.WriteString("<a href=\"-/\">default</a>\n")
|
||||
c.Writer.WriteString("<a href=\"-/\">default</a>\n")
|
||||
}
|
||||
for publishPrefix := range localRepos {
|
||||
_, _ = c.Writer.WriteString(fmt.Sprintf("<a href=\"%[1]s/\">%[1]s</a>\n", publishPrefix))
|
||||
c.Writer.WriteString(fmt.Sprintf("<a href=\"%[1]s/\">%[1]s</a>\n", publishPrefix))
|
||||
}
|
||||
_, _ = c.Writer.WriteString("</pre>")
|
||||
c.Writer.WriteString("</pre>")
|
||||
c.Writer.Flush()
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,7 @@ func apiReposList(c *gin.Context) {
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.LocalRepoCollection()
|
||||
_ = collection.ForEach(func(r *deb.LocalRepo) error {
|
||||
collection.ForEach(func(r *deb.LocalRepo) error {
|
||||
result = append(result, r)
|
||||
return nil
|
||||
})
|
||||
@@ -95,6 +95,8 @@ type repoCreateParams struct {
|
||||
DefaultComponent string ` json:"DefaultComponent" example:"main"`
|
||||
// Snapshot name to create repoitory from (optional)
|
||||
FromSnapshot string ` json:"FromSnapshot" example:""`
|
||||
//
|
||||
LdapGroup string
|
||||
}
|
||||
|
||||
// @Summary Create Repository
|
||||
@@ -107,9 +109,9 @@ type repoCreateParams struct {
|
||||
// @Description {"Name":"aptly-repo","Comment":"","DefaultDistribution":"","DefaultComponent":""}
|
||||
// @Description ```
|
||||
// @Tags Repos
|
||||
// @Produce json
|
||||
// @Consume json
|
||||
// @Param request body repoCreateParams true "Parameters"
|
||||
// @Produce json
|
||||
// @Success 201 {object} deb.LocalRepo
|
||||
// @Failure 404 {object} Error "Source snapshot not found"
|
||||
// @Failure 409 {object} Error "Local repo already exists"
|
||||
@@ -125,6 +127,7 @@ func apiReposCreate(c *gin.Context) {
|
||||
repo := deb.NewLocalRepo(b.Name, b.Comment)
|
||||
repo.DefaultComponent = b.DefaultComponent
|
||||
repo.DefaultDistribution = b.DefaultDistribution
|
||||
repo.LdapGroup = b.LdapGroup
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
|
||||
@@ -173,15 +176,15 @@ type reposEditParams struct {
|
||||
DefaultDistribution *string ` json:"DefaultDistribution" example:""`
|
||||
// Change Devault Component for publishing
|
||||
DefaultComponent *string ` json:"DefaultComponent" example:""`
|
||||
//
|
||||
LdapGroup *string
|
||||
}
|
||||
|
||||
// @Summary Update Repository
|
||||
// @Description **Update local repository meta information**
|
||||
// @Tags Repos
|
||||
// @Param name path string true "Repository name"
|
||||
// @Consume json
|
||||
// @Param request body reposEditParams true "Parameters"
|
||||
// @Produce json
|
||||
// @Param request body reposEditParams true "Parameters"
|
||||
// @Success 200 {object} deb.LocalRepo "msg"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Failure 500 {object} Error "Internal Server Error"
|
||||
@@ -201,6 +204,12 @@ func apiReposEdit(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
err = CheckGroup(c, repo.LdapGroup)
|
||||
if err != nil {
|
||||
c.AbortWithError(403, err)
|
||||
return
|
||||
}
|
||||
|
||||
if b.Name != nil {
|
||||
_, err := collection.ByName(*b.Name)
|
||||
if err == nil {
|
||||
@@ -219,6 +228,9 @@ func apiReposEdit(c *gin.Context) {
|
||||
if b.DefaultComponent != nil {
|
||||
repo.DefaultComponent = *b.DefaultComponent
|
||||
}
|
||||
if b.LdapGroup != nil {
|
||||
repo.LdapGroup = *b.LdapGroup
|
||||
}
|
||||
|
||||
err = collection.Update(repo)
|
||||
if err != nil {
|
||||
@@ -233,8 +245,8 @@ func apiReposEdit(c *gin.Context) {
|
||||
// @Summary Get Repository Info
|
||||
// @Description Returns basic information about local repository.
|
||||
// @Tags Repos
|
||||
// @Param name path string true "Repository name"
|
||||
// @Produce json
|
||||
// @Param name path string true "Repository name"
|
||||
// @Success 200 {object} deb.LocalRepo
|
||||
// @Failure 404 {object} Error "Repository not found"
|
||||
// @Router /api/repos/{name} [get]
|
||||
@@ -256,10 +268,9 @@ func apiReposShow(c *gin.Context) {
|
||||
// @Description Cannot drop repos that are published.
|
||||
// @Description Needs force=1 to drop repos used as source by other repos.
|
||||
// @Tags Repos
|
||||
// @Param name path string true "Repository name"
|
||||
// @Produce json
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Param force query int false "force: 1 to enable"
|
||||
// @Produce json
|
||||
// @Success 200 {object} task.ProcessReturnValue "Repo object"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Failure 404 {object} Error "Repo Conflict"
|
||||
@@ -279,6 +290,12 @@ func apiReposDrop(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
err = CheckGroup(c, repo.LdapGroup)
|
||||
if err != nil {
|
||||
c.AbortWithError(403, err)
|
||||
return
|
||||
}
|
||||
|
||||
resources := []string{string(repo.Key())}
|
||||
taskName := fmt.Sprintf("Delete repo %s", name)
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
@@ -309,12 +326,12 @@ func apiReposDrop(c *gin.Context) {
|
||||
// @Description ["Pi386 aptly 0.8 966561016b44ed80"]
|
||||
// @Description ```
|
||||
// @Tags Repos
|
||||
// @Param name path string true "Repository name"
|
||||
// @Produce json
|
||||
// @Param name path string true "Snapshot to search"
|
||||
// @Param q query string true "Package query (e.g Name%20(~%20matlab))"
|
||||
// @Param withDeps query string true "Set to 1 to include dependencies when evaluating package query"
|
||||
// @Param format query string true "Set to 'details' to return extra info about each package"
|
||||
// @Param maximumVersion query string true "Set to 1 to only return the highest version for each package name"
|
||||
// @Produce json
|
||||
// @Success 200 {object} string "msg"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Failure 404 {object} Error "Internal Server Error"
|
||||
@@ -368,6 +385,11 @@ func apiReposPackagesAddDelete(c *gin.Context, taskNamePrefix string, cb func(li
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
|
||||
err = CheckGroup(c, repo.LdapGroup)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: 403, Value: nil}, err
|
||||
}
|
||||
|
||||
out.Printf("Loading packages...\n")
|
||||
list, err := deb.NewPackageListFromRefList(repo.RefList(), collectionFactory.PackageCollection(), nil)
|
||||
if err != nil {
|
||||
@@ -409,10 +431,9 @@ func apiReposPackagesAddDelete(c *gin.Context, taskNamePrefix string, cb func(li
|
||||
// @Description
|
||||
// @Description API verifies that packages actually exist in aptly database and checks constraint that conflicting packages can’t be part of the same local repository.
|
||||
// @Tags Repos
|
||||
// @Param name path string true "Repository name"
|
||||
// @Produce json
|
||||
// @Param request body reposPackagesAddDeleteParams true "Parameters"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Produce json
|
||||
// @Success 200 {object} string "msg"
|
||||
// @Failure 400 {object} Error "Bad Request"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
@@ -430,11 +451,9 @@ func apiReposPackagesAdd(c *gin.Context) {
|
||||
// @Description
|
||||
// @Description Any package(s) can be removed from a local repository. Package references from a local repository can be retrieved with GET /api/repos/:name/packages.
|
||||
// @Tags Repos
|
||||
// @Param name path string true "Repository name"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Consume json
|
||||
// @Param request body reposPackagesAddDeleteParams true "Parameters"
|
||||
// @Produce json
|
||||
// @Param request body reposPackagesAddDeleteParams true "Parameters"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Success 200 {object} string "msg"
|
||||
// @Failure 400 {object} Error "Bad Request"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
@@ -528,6 +547,11 @@ func apiReposPackageFromDir(c *gin.Context) {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
|
||||
err = CheckGroup(c, repo.LdapGroup)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: 403, Value: nil}, err
|
||||
}
|
||||
|
||||
verifier := context.GetVerifier()
|
||||
|
||||
var (
|
||||
@@ -576,7 +600,7 @@ func apiReposPackageFromDir(c *gin.Context) {
|
||||
}
|
||||
|
||||
// atempt to remove dir, if it fails, that's fine: probably it's not empty
|
||||
_ = os.Remove(filepath.Join(context.UploadPath(), dirParam))
|
||||
os.Remove(filepath.Join(context.UploadPath(), dirParam))
|
||||
}
|
||||
|
||||
if failedFiles == nil {
|
||||
@@ -614,8 +638,8 @@ type reposCopyPackageParams struct {
|
||||
// @Description Copies a package from a source to destination repository
|
||||
// @Tags Repos
|
||||
// @Produce json
|
||||
// @Param name path string true "Destination repo"
|
||||
// @Param src path string true "Source repo"
|
||||
// @Param name path string true "Source repo"
|
||||
// @Param src path string true "Destination repo"
|
||||
// @Param file path string true "File/packages to copy"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Success 200 {object} task.ProcessReturnValue "msg"
|
||||
@@ -768,15 +792,12 @@ func apiReposCopyPackage(c *gin.Context) {
|
||||
// @Summary Include File from Directory
|
||||
// @Description Allows automatic processing of .changes file controlling package upload (uploaded using File Upload API) to the local repository. i.e. Exposes repo include command in api.
|
||||
// @Tags Repos
|
||||
// @Param name path string true "Repository name"
|
||||
// @Param dir path string true "Directory of packages"
|
||||
// @Param file path string true "File/packages to include"
|
||||
// @Produce json
|
||||
// @Param forceReplace query int false "when value is set to 1, when adding package that conflicts with existing package, remove existing package"
|
||||
// @Param noRemoveFiles query int false "when value is set to 1, don’t remove files that have been imported successfully into repository"
|
||||
// @Param acceptUnsigned query int false "when value is set to 1, accept unsigned .changes files"
|
||||
// @Param ignoreSignature query int false "when value is set to 1 disable verification of .changes file signature"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Produce json
|
||||
// @Success 200 {object} string "msg"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Router /api/repos/{name}/include/{dir}/{file} [post]
|
||||
@@ -785,22 +806,26 @@ func apiReposIncludePackageFromFile(c *gin.Context) {
|
||||
apiReposIncludePackageFromDir(c)
|
||||
}
|
||||
|
||||
type reposIncludePackageFromDirReport struct {
|
||||
Warnings []string
|
||||
Added []string
|
||||
Deleted []string
|
||||
}
|
||||
|
||||
type reposIncludePackageFromDirResponse struct {
|
||||
Report *aptly.RecordingResultReporter
|
||||
Report reposIncludePackageFromDirReport
|
||||
FailedFiles []string
|
||||
}
|
||||
|
||||
// @Summary Include Directory
|
||||
// @Description Allows automatic processing of .changes file controlling package upload (uploaded using File Upload API) to the local repository. i.e. Exposes repo include command in api.
|
||||
// @Tags Repos
|
||||
// @Param name path string true "Repository name"
|
||||
// @Param dir path string true "Directory of packages"
|
||||
// @Produce json
|
||||
// @Param forceReplace query int false "when value is set to 1, when adding package that conflicts with existing package, remove existing package"
|
||||
// @Param noRemoveFiles query int false "when value is set to 1, don’t remove files that have been imported successfully into repository"
|
||||
// @Param acceptUnsigned query int false "when value is set to 1, accept unsigned .changes files"
|
||||
// @Param ignoreSignature query int false "when value is set to 1 disable verification of .changes file signature"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Produce json
|
||||
// @Success 200 {object} reposIncludePackageFromDirResponse "Response"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Router /api/repos/{name}/include/{dir} [post]
|
||||
@@ -841,7 +866,7 @@ func apiReposIncludePackageFromDir(c *gin.Context) {
|
||||
}
|
||||
|
||||
var resources []string
|
||||
if len(repoTemplate.Root.Nodes) > 1 {
|
||||
if len(repoTemplate.Tree.Root.Nodes) > 1 {
|
||||
resources = append(resources, task.AllLocalReposResourcesKey)
|
||||
} else {
|
||||
// repo template string is simple text so only use resource key of specific repository
|
||||
@@ -850,6 +875,11 @@ func apiReposIncludePackageFromDir(c *gin.Context) {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
err = CheckGroup(c, repo.LdapGroup)
|
||||
if err != nil {
|
||||
c.AbortWithError(403, err)
|
||||
return
|
||||
}
|
||||
|
||||
resources = append(resources, string(repo.Key()))
|
||||
}
|
||||
@@ -881,7 +911,7 @@ func apiReposIncludePackageFromDir(c *gin.Context) {
|
||||
|
||||
if !noRemoveFiles {
|
||||
// atempt to remove dir, if it fails, that's fine: probably it's not empty
|
||||
_ = os.Remove(filepath.Join(context.UploadPath(), dirParam))
|
||||
os.Remove(filepath.Join(context.UploadPath(), dirParam))
|
||||
}
|
||||
|
||||
if failedFiles == nil {
|
||||
@@ -901,10 +931,9 @@ func apiReposIncludePackageFromDir(c *gin.Context) {
|
||||
out.Printf("Failed files: %s\n", strings.Join(failedFiles, ", "))
|
||||
}
|
||||
|
||||
ret := reposIncludePackageFromDirResponse{
|
||||
Report: reporter,
|
||||
FailedFiles: failedFiles,
|
||||
}
|
||||
return &task.ProcessReturnValue{Code: http.StatusOK, Value: ret}, nil
|
||||
return &task.ProcessReturnValue{Code: http.StatusOK, Value: gin.H{
|
||||
"Report": reporter,
|
||||
"FailedFiles": failedFiles,
|
||||
}}, nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,591 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type ReposTestSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&ReposTestSuite{})
|
||||
|
||||
func (s *ReposTestSuite) SetUpTest(c *C) {
|
||||
s.APISuite.SetUpTest(c)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposListEmpty(c *C) {
|
||||
// Test listing repos when none exist
|
||||
req, _ := http.NewRequest("GET", "/api/repos", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
c.Check(w.Header().Get("Content-Type"), Equals, "application/json; charset=utf-8")
|
||||
|
||||
var result []*deb.LocalRepo
|
||||
err := json.Unmarshal(w.Body.Bytes(), &result)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(len(result), Equals, 0)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposCreateBasic(c *C) {
|
||||
// Test creating a basic repository
|
||||
params := repoCreateParams{
|
||||
Name: "test-repo",
|
||||
Comment: "Test repository",
|
||||
DefaultDistribution: "stable",
|
||||
DefaultComponent: "main",
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/repos", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Now context is properly set up, should create successfully
|
||||
c.Check(w.Code, Equals, 201) // Expect successful creation
|
||||
|
||||
// Clean up: delete the created repo
|
||||
req, _ = http.NewRequest("DELETE", "/api/repos/test-repo?force=1", nil)
|
||||
w = httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Check(w.Code, Equals, 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposEdit(c *C) {
|
||||
// First create a repo
|
||||
params := repoCreateParams{
|
||||
Name: "edit-test-repo",
|
||||
Comment: "Original comment",
|
||||
}
|
||||
body, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/repos", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Check(w.Code, Equals, 201)
|
||||
|
||||
// Now edit it
|
||||
editParams := reposEditParams{
|
||||
Comment: stringPtr("Updated comment"),
|
||||
}
|
||||
body, _ = json.Marshal(editParams)
|
||||
req, _ = http.NewRequest("PUT", "/api/repos/edit-test-repo", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w = httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Check(w.Code, Equals, 200)
|
||||
|
||||
// Clean up
|
||||
req, _ = http.NewRequest("DELETE", "/api/repos/edit-test-repo?force=1", nil)
|
||||
w = httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Check(w.Code, Equals, 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposPackagesAddDelete(c *C) {
|
||||
// First create a repo
|
||||
params := repoCreateParams{
|
||||
Name: "pkg-test-repo",
|
||||
}
|
||||
body, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/repos", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Check(w.Code, Equals, 201)
|
||||
|
||||
// Test adding packages (will fail without actual packages)
|
||||
addParams := reposPackagesAddDeleteParams{
|
||||
PackageRefs: []string{"Pamd64 test 1.0 abc123"},
|
||||
}
|
||||
body, _ = json.Marshal(addParams)
|
||||
req, _ = http.NewRequest("POST", "/api/repos/pkg-test-repo/packages", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w = httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
// Will fail as package doesn't exist
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
|
||||
// Clean up
|
||||
req, _ = http.NewRequest("DELETE", "/api/repos/pkg-test-repo?force=1", nil)
|
||||
w = httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Check(w.Code, Equals, 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposCopyPackage(c *C) {
|
||||
// Create source and destination repos
|
||||
params := repoCreateParams{Name: "src-repo"}
|
||||
body, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/repos", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Check(w.Code, Equals, 201)
|
||||
|
||||
params = repoCreateParams{Name: "dst-repo"}
|
||||
body, _ = json.Marshal(params)
|
||||
req, _ = http.NewRequest("POST", "/api/repos", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w = httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Check(w.Code, Equals, 201)
|
||||
|
||||
// Test copy (will fail without packages)
|
||||
copyParams := reposCopyPackageParams{
|
||||
WithDeps: true,
|
||||
DryRun: true,
|
||||
}
|
||||
body, _ = json.Marshal(copyParams)
|
||||
req, _ = http.NewRequest("POST", "/api/repos/dst-repo/copy/src-repo/test", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w = httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
// Will return empty result as no packages match
|
||||
c.Check(w.Code, Equals, 200)
|
||||
|
||||
// Clean up
|
||||
req, _ = http.NewRequest("DELETE", "/api/repos/src-repo?force=1", nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
req, _ = http.NewRequest("DELETE", "/api/repos/dst-repo?force=1", nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposCreateInvalidJSON(c *C) {
|
||||
// Test creating repository with invalid JSON
|
||||
req, _ := http.NewRequest("POST", "/api/repos", bytes.NewBufferString("invalid json"))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 400)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposCreateMissingName(c *C) {
|
||||
// Test creating repository without required name
|
||||
params := repoCreateParams{
|
||||
Comment: "Test repository",
|
||||
DefaultDistribution: "stable",
|
||||
DefaultComponent: "main",
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/repos", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 400)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposShowNotFound(c *C) {
|
||||
// Test showing non-existent repository
|
||||
req, _ := http.NewRequest("GET", "/api/repos/nonexistent", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests endpoint structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposEditStructure(c *C) {
|
||||
// Test repository edit endpoint structure
|
||||
params := reposEditParams{
|
||||
Name: stringPtr("new-name"),
|
||||
Comment: stringPtr("Updated comment"),
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("PUT", "/api/repos/test-repo", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposEditInvalidJSON(c *C) {
|
||||
// Test edit with invalid JSON
|
||||
req, _ := http.NewRequest("PUT", "/api/repos/test-repo", bytes.NewBufferString("invalid"))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 400)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposDropStructure(c *C) {
|
||||
// Test repository drop endpoint structure
|
||||
req, _ := http.NewRequest("DELETE", "/api/repos/test-repo", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should return 404 as test-repo doesn't exist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposDropWithForce(c *C) {
|
||||
// Test repository drop with force parameter
|
||||
req, _ := http.NewRequest("DELETE", "/api/repos/test-repo?force=1", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests parameter parsing
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposPackagesShowStructure(c *C) {
|
||||
// Test packages show endpoint structure
|
||||
req, _ := http.NewRequest("GET", "/api/repos/test-repo/packages", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposPackagesShowWithQuery(c *C) {
|
||||
// Test packages show with query parameters
|
||||
req, _ := http.NewRequest("GET", "/api/repos/test-repo/packages?q=Name%20(~%20test)", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests query parsing
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposPackagesAddStructure(c *C) {
|
||||
// Test packages add endpoint structure
|
||||
params := reposPackagesAddDeleteParams{
|
||||
PackageRefs: []string{"Pamd64 test 1.0 abc123"},
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/repos/test-repo/packages", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposPackagesAddInvalidJSON(c *C) {
|
||||
// Test packages add with invalid JSON
|
||||
req, _ := http.NewRequest("POST", "/api/repos/test-repo/packages", bytes.NewBufferString("invalid"))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 400)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposPackagesDeleteStructure(c *C) {
|
||||
// Test packages delete endpoint structure
|
||||
params := reposPackagesAddDeleteParams{
|
||||
PackageRefs: []string{"Pamd64 test 1.0 abc123"},
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("DELETE", "/api/repos/test-repo/packages", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposFileUploadStructure(c *C) {
|
||||
// Test file upload endpoint structure
|
||||
req, _ := http.NewRequest("POST", "/api/repos/test-repo/file/upload-dir", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposFileUploadWithParameters(c *C) {
|
||||
// Test file upload with query parameters
|
||||
req, _ := http.NewRequest("POST", "/api/repos/test-repo/file/upload-dir?noRemove=1&forceReplace=1", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests parameter parsing
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposFileUploadSpecificFile(c *C) {
|
||||
// Test specific file upload endpoint
|
||||
req, _ := http.NewRequest("POST", "/api/repos/test-repo/file/upload-dir/package.deb", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposCopyPackageStructure(c *C) {
|
||||
// Test copy package endpoint structure
|
||||
params := reposCopyPackageParams{
|
||||
WithDeps: true,
|
||||
DryRun: false,
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/repos/dest-repo/copy/src-repo/package-query", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposCopyPackageInvalidJSON(c *C) {
|
||||
// Test copy package with invalid JSON
|
||||
req, _ := http.NewRequest("POST", "/api/repos/dest-repo/copy/src-repo/package-query", bytes.NewBufferString("invalid"))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 400)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposIncludePackageStructure(c *C) {
|
||||
// Test include package endpoint structure
|
||||
req, _ := http.NewRequest("POST", "/api/repos/test-repo/include/upload-dir", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposIncludePackageWithParameters(c *C) {
|
||||
// Test include package with query parameters
|
||||
req, _ := http.NewRequest("POST", "/api/repos/test-repo/include/upload-dir?forceReplace=1&noRemoveFiles=1&acceptUnsigned=1&ignoreSignature=1", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests parameter parsing
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposIncludeSpecificFile(c *C) {
|
||||
// Test include specific file endpoint
|
||||
req, _ := http.NewRequest("POST", "/api/repos/test-repo/include/upload-dir/package.changes", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposParameterValidation(c *C) {
|
||||
// Test parameter validation and structure
|
||||
testCases := []struct {
|
||||
name string
|
||||
method string
|
||||
path string
|
||||
body string
|
||||
wantCode int
|
||||
}{
|
||||
{"invalid repo name chars", "GET", "/api/repos/invalid/name", "", 404}, // route doesn't match
|
||||
{"empty repo name", "GET", "/api/repos", "", 200}, // list repos endpoint
|
||||
{"invalid method", "PATCH", "/api/repos/test", "", 404},
|
||||
{"malformed JSON in create", "POST", "/api/repos", `{"Name":}`, 400},
|
||||
{"malformed JSON in edit", "PUT", "/api/repos/test", `{"Name":}`, 400},
|
||||
{"malformed JSON in packages", "POST", "/api/repos/test/packages", `{"PackageRefs":}`, 400},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
var req *http.Request
|
||||
if tc.body != "" {
|
||||
req, _ = http.NewRequest(tc.method, tc.path, strings.NewReader(tc.body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
} else {
|
||||
req, _ = http.NewRequest(tc.method, tc.path, nil)
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, tc.wantCode, Commentf("Test case: %s", tc.name))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposListInAPIModeStructure(c *C) {
|
||||
// Test reposListInAPIMode function structure
|
||||
localRepos := map[string]utils.FileSystemPublishRoot{
|
||||
"repo1": {},
|
||||
"repo2": {},
|
||||
}
|
||||
|
||||
handler := reposListInAPIMode(localRepos)
|
||||
c.Check(handler, NotNil)
|
||||
|
||||
// Test with empty repos map
|
||||
emptyHandler := reposListInAPIMode(map[string]utils.FileSystemPublishRoot{})
|
||||
c.Check(emptyHandler, NotNil)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposServeInAPIModeStructure(c *C) {
|
||||
// Test reposServeInAPIMode function structure by simulating call
|
||||
s.router.(*gin.Engine).GET("/api/:storage/*pkgPath", reposServeInAPIMode)
|
||||
|
||||
// Test with default storage
|
||||
req, _ := http.NewRequest("GET", "/api/-/some/package/path", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests parameter parsing
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
|
||||
// Test with specific storage
|
||||
req, _ = http.NewRequest("GET", "/api/storage1/some/package/path", nil)
|
||||
w = httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposCreateFromSnapshot(c *C) {
|
||||
// Test creating repository from snapshot
|
||||
params := repoCreateParams{
|
||||
Name: "test-repo-from-snapshot",
|
||||
Comment: "Test repository from snapshot",
|
||||
FromSnapshot: "test-snapshot",
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/repos", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context/snapshot, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposPackagesAsyncOperations(c *C) {
|
||||
// Test async operations with _async parameter
|
||||
params := reposPackagesAddDeleteParams{
|
||||
PackageRefs: []string{"Pamd64 test 1.0 abc123"},
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/repos/test-repo/packages?_async=1", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests async parameter parsing
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposDropAsyncOperation(c *C) {
|
||||
// Test async repository drop
|
||||
req, _ := http.NewRequest("DELETE", "/api/repos/test-repo?_async=1&force=1", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests async parameter parsing
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposCopyAsyncOperation(c *C) {
|
||||
// Test async copy operation
|
||||
params := reposCopyPackageParams{
|
||||
WithDeps: false,
|
||||
DryRun: true,
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/repos/dest-repo/copy/src-repo/package-query?_async=1", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
// Helper function to create string pointer
|
||||
func stringPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposPathSanitization(c *C) {
|
||||
// Test path sanitization in file operations
|
||||
testPaths := []string{
|
||||
"../../../etc/passwd",
|
||||
"normal-dir",
|
||||
"dir with spaces",
|
||||
".hidden-dir",
|
||||
"",
|
||||
}
|
||||
|
||||
for _, path := range testPaths {
|
||||
// Test sanitization doesn't cause crashes
|
||||
sanitized := utils.SanitizePath(path)
|
||||
c.Check(sanitized, NotNil)
|
||||
|
||||
// Test with file upload endpoints
|
||||
req, _ := http.NewRequest("POST", fmt.Sprintf("/api/repos/test-repo/file/%s", path), nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should not crash, even if it errors due to missing context
|
||||
c.Check(w.Code, Not(Equals), 0)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposErrorHandling(c *C) {
|
||||
// Test various error conditions and edge cases
|
||||
errorTests := []struct {
|
||||
description string
|
||||
method string
|
||||
path string
|
||||
body string
|
||||
expectedErr bool
|
||||
}{
|
||||
{"Missing required fields", "POST", "/api/repos", `{}`, true},
|
||||
{"Invalid package refs", "POST", "/api/repos/test/packages", `{"PackageRefs":[]}`, true},
|
||||
{"Invalid query format", "GET", "/api/repos/test/packages?q=invalid[query", "", false}, // Query validation happens deeper
|
||||
{"Copy to same repo", "POST", "/api/repos/test/copy/test/pkg", `{}`, false}, // Error happens in business logic
|
||||
{"File upload endpoint", "POST", "/api/repos/test/file/upload-dir", "", false}, // Valid endpoint
|
||||
}
|
||||
|
||||
for _, test := range errorTests {
|
||||
var req *http.Request
|
||||
if test.body != "" {
|
||||
req, _ = http.NewRequest(test.method, test.path, strings.NewReader(test.body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
} else {
|
||||
req, _ = http.NewRequest(test.method, test.path, nil)
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// All should return some response without crashing
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Test: %s", test.description))
|
||||
}
|
||||
}
|
||||
+149
-67
@@ -1,8 +1,12 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
ctx "github.com/aptly-dev/aptly/context"
|
||||
@@ -14,6 +18,10 @@ import (
|
||||
"github.com/aptly-dev/aptly/docs"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-contrib/sessions/cookie"
|
||||
"github.com/gin-gonic/gin"
|
||||
uuid "github.com/nu7hatch/gouuid"
|
||||
)
|
||||
|
||||
var context *ctx.AptlyContext
|
||||
@@ -55,9 +63,13 @@ func Router(c *ctx.AptlyContext) http.Handler {
|
||||
router.UseRawPath = true
|
||||
|
||||
if c.Config().LogFormat == "json" {
|
||||
c.StructuredLogging(true)
|
||||
utils.SetupJSONLogger(c.Config().LogLevel, os.Stdout)
|
||||
gin.DefaultWriter = utils.LogWriter{Logger: log.Logger}
|
||||
router.Use(JSONLogger())
|
||||
} else {
|
||||
c.StructuredLogging(false)
|
||||
utils.SetupDefaultLogger(c.Config().LogLevel)
|
||||
router.Use(gin.Logger())
|
||||
}
|
||||
|
||||
@@ -77,7 +89,7 @@ func Router(c *ctx.AptlyContext) http.Handler {
|
||||
}
|
||||
|
||||
if c.Config().ServeInAPIMode {
|
||||
router.GET("/repos/", reposListInAPIMode(c.Config().GetFileSystemPublishRoots()))
|
||||
router.GET("/repos/", reposListInAPIMode(c.Config().FileSystemPublishRoots))
|
||||
router.GET("/repos/:storage/*pkgPath", reposServeInAPIMode)
|
||||
}
|
||||
|
||||
@@ -86,17 +98,25 @@ func Router(c *ctx.AptlyContext) http.Handler {
|
||||
// We use a goroutine to count the number of
|
||||
// concurrent requests. When no more requests are
|
||||
// running, we close the database to free the lock.
|
||||
initDBRequests()
|
||||
dbRequests = make(chan dbRequest)
|
||||
|
||||
go acquireDatabase()
|
||||
|
||||
api.Use(func(c *gin.Context) {
|
||||
err := acquireDatabaseConnection()
|
||||
var err error
|
||||
|
||||
errCh := make(chan error)
|
||||
dbRequests <- dbRequest{acquiredb, errCh}
|
||||
|
||||
err = <-errCh
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := releaseDatabaseConnection()
|
||||
dbRequests <- dbRequest{releasedb, errCh}
|
||||
err = <-errCh
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
}
|
||||
@@ -120,105 +140,167 @@ func Router(c *ctx.AptlyContext) http.Handler {
|
||||
api.GET("/healthy", apiHealthy)
|
||||
}
|
||||
|
||||
// set up cookies and sessions
|
||||
token, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
store := cookie.NewStore([]byte(token.String()))
|
||||
router.Use(sessions.Sessions(token.String(), store))
|
||||
// prep our config fetcher ahead of need
|
||||
config := context.Config()
|
||||
|
||||
// prep a logfile if we've set one
|
||||
if config.LogFile != "" {
|
||||
file, err := os.OpenFile(config.LogFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer file.Close()
|
||||
log.SetOutput(file)
|
||||
}
|
||||
|
||||
router.GET("/version", apiVersion)
|
||||
|
||||
var username string
|
||||
var password string
|
||||
router.POST("/login", func(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
session.Options(sessions.Options{MaxAge: 30})
|
||||
if config.UseAuth {
|
||||
log.Printf("UseAuth is enabled\n")
|
||||
username = c.PostForm("username")
|
||||
password = c.PostForm("password")
|
||||
if !Authorize(username, password) {
|
||||
c.AbortWithError(403, fmt.Errorf("Authorization Failure"))
|
||||
}
|
||||
log.Printf("%s authorized from %s\n", username, c.ClientIP())
|
||||
}
|
||||
session.Set(token.String(), time.Now().Unix())
|
||||
session.Save()
|
||||
getGroups(c, username)
|
||||
c.String(200, "Authorized!")
|
||||
})
|
||||
|
||||
router.POST("/logout", func(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
session.Options(sessions.Options{MaxAge: -1})
|
||||
session.Save()
|
||||
c.String(200, "Deauthorized")
|
||||
})
|
||||
|
||||
authorize := router.Group("/api", func(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
if config.UseAuth {
|
||||
if session.Get(token.String()) == nil {
|
||||
c.AbortWithError(403, fmt.Errorf("not authorized"))
|
||||
}
|
||||
session.Options(sessions.Options{MaxAge: 30})
|
||||
session.Set(token.String(), time.Now().Unix())
|
||||
session.Save()
|
||||
}
|
||||
})
|
||||
|
||||
{
|
||||
api.GET("/repos", apiReposList)
|
||||
api.POST("/repos", apiReposCreate)
|
||||
api.GET("/repos/:name", apiReposShow)
|
||||
api.PUT("/repos/:name", apiReposEdit)
|
||||
api.DELETE("/repos/:name", apiReposDrop)
|
||||
authorize.GET("/repos", apiReposList)
|
||||
authorize.POST("/repos", apiReposCreate)
|
||||
authorize.GET("/repos/:name", apiReposShow)
|
||||
authorize.PUT("/repos/:name", apiReposEdit)
|
||||
authorize.DELETE("/repos/:name", apiReposDrop)
|
||||
|
||||
api.GET("/repos/:name/packages", apiReposPackagesShow)
|
||||
api.POST("/repos/:name/packages", apiReposPackagesAdd)
|
||||
api.DELETE("/repos/:name/packages", apiReposPackagesDelete)
|
||||
authorize.GET("/repos/:name/packages", apiReposPackagesShow)
|
||||
authorize.POST("/repos/:name/packages", apiReposPackagesAdd)
|
||||
authorize.DELETE("/repos/:name/packages", apiReposPackagesDelete)
|
||||
|
||||
api.POST("/repos/:name/file/:dir/:file", apiReposPackageFromFile)
|
||||
api.POST("/repos/:name/file/:dir", apiReposPackageFromDir)
|
||||
api.POST("/repos/:name/copy/:src/:file", apiReposCopyPackage)
|
||||
authorize.POST("/repos/:name/file/:dir/:file", apiReposPackageFromFile)
|
||||
authorize.POST("/repos/:name/file/:dir", apiReposPackageFromDir)
|
||||
authorize.POST("/repos/:name/copy/:src/:file", apiReposCopyPackage)
|
||||
|
||||
api.POST("/repos/:name/include/:dir/:file", apiReposIncludePackageFromFile)
|
||||
api.POST("/repos/:name/include/:dir", apiReposIncludePackageFromDir)
|
||||
authorize.POST("/repos/:name/include/:dir/:file", apiReposIncludePackageFromFile)
|
||||
authorize.POST("/repos/:name/include/:dir", apiReposIncludePackageFromDir)
|
||||
|
||||
api.POST("/repos/:name/snapshots", apiSnapshotsCreateFromRepository)
|
||||
authorize.POST("/repos/:name/snapshots", apiSnapshotsCreateFromRepository)
|
||||
}
|
||||
|
||||
{
|
||||
api.POST("/mirrors/:name/snapshots", apiSnapshotsCreateFromMirror)
|
||||
authorize.POST("/mirrors/:name/snapshots", apiSnapshotsCreateFromMirror)
|
||||
}
|
||||
|
||||
{
|
||||
api.GET("/mirrors", apiMirrorsList)
|
||||
api.GET("/mirrors/:name", apiMirrorsShow)
|
||||
api.GET("/mirrors/:name/packages", apiMirrorsPackages)
|
||||
api.POST("/mirrors", apiMirrorsCreate)
|
||||
api.PUT("/mirrors/:name", apiMirrorsUpdate)
|
||||
api.DELETE("/mirrors/:name", apiMirrorsDrop)
|
||||
authorize.GET("/mirrors", apiMirrorsList)
|
||||
authorize.GET("/mirrors/:name", apiMirrorsShow)
|
||||
authorize.GET("/mirrors/:name/packages", apiMirrorsPackages)
|
||||
authorize.POST("/mirrors", apiMirrorsCreate)
|
||||
authorize.PUT("/mirrors/:name", apiMirrorsUpdate)
|
||||
authorize.DELETE("/mirrors/:name", apiMirrorsDrop)
|
||||
}
|
||||
|
||||
{
|
||||
api.POST("/gpg/key", apiGPGAddKey)
|
||||
authorize.POST("/gpg/key", apiGPGAddKey)
|
||||
}
|
||||
|
||||
{
|
||||
api.GET("/s3", apiS3List)
|
||||
authorize.GET("/s3", apiS3List)
|
||||
}
|
||||
|
||||
{
|
||||
api.GET("/files", apiFilesListDirs)
|
||||
api.POST("/files/:dir", apiFilesUpload)
|
||||
api.GET("/files/:dir", apiFilesListFiles)
|
||||
api.DELETE("/files/:dir", apiFilesDeleteDir)
|
||||
api.DELETE("/files/:dir/:name", apiFilesDeleteFile)
|
||||
authorize.GET("/files", apiFilesListDirs)
|
||||
authorize.POST("/files/:dir", apiFilesUpload)
|
||||
authorize.GET("/files/:dir", apiFilesListFiles)
|
||||
authorize.DELETE("/files/:dir", apiFilesDeleteDir)
|
||||
authorize.DELETE("/files/:dir/:name", apiFilesDeleteFile)
|
||||
}
|
||||
|
||||
{
|
||||
api.GET("/publish", apiPublishList)
|
||||
api.GET("/publish/:prefix/:distribution", apiPublishShow)
|
||||
api.POST("/publish", apiPublishRepoOrSnapshot)
|
||||
api.POST("/publish/:prefix", apiPublishRepoOrSnapshot)
|
||||
api.PUT("/publish/:prefix/:distribution", apiPublishUpdateSwitch)
|
||||
api.DELETE("/publish/:prefix/:distribution", apiPublishDrop)
|
||||
api.POST("/publish/:prefix/:distribution/sources", apiPublishAddSource)
|
||||
api.GET("/publish/:prefix/:distribution/sources", apiPublishListChanges)
|
||||
api.PUT("/publish/:prefix/:distribution/sources", apiPublishSetSources)
|
||||
api.DELETE("/publish/:prefix/:distribution/sources", apiPublishDropChanges)
|
||||
api.PUT("/publish/:prefix/:distribution/sources/:component", apiPublishUpdateSource)
|
||||
api.DELETE("/publish/:prefix/:distribution/sources/:component", apiPublishRemoveSource)
|
||||
api.POST("/publish/:prefix/:distribution/update", apiPublishUpdate)
|
||||
authorize.GET("/publish", apiPublishList)
|
||||
authorize.GET("/publish/:prefix/:distribution", apiPublishShow)
|
||||
authorize.POST("/publish", apiPublishRepoOrSnapshot)
|
||||
authorize.POST("/publish/:prefix", apiPublishRepoOrSnapshot)
|
||||
authorize.PUT("/publish/:prefix/:distribution", apiPublishUpdateSwitch)
|
||||
authorize.DELETE("/publish/:prefix/:distribution", apiPublishDrop)
|
||||
authorize.POST("/publish/:prefix/:distribution/sources", apiPublishAddSource)
|
||||
authorize.GET("/publish/:prefix/:distribution/sources", apiPublishListChanges)
|
||||
authorize.PUT("/publish/:prefix/:distribution/sources", apiPublishSetSources)
|
||||
authorize.DELETE("/publish/:prefix/:distribution/sources", apiPublishDropChanges)
|
||||
authorize.PUT("/publish/:prefix/:distribution/sources/:component", apiPublishUpdateSource)
|
||||
authorize.DELETE("/publish/:prefix/:distribution/sources/:component", apiPublishRemoveSource)
|
||||
authorize.POST("/publish/:prefix/:distribution/update", apiPublishUpdate)
|
||||
}
|
||||
|
||||
{
|
||||
api.GET("/snapshots", apiSnapshotsList)
|
||||
api.POST("/snapshots", apiSnapshotsCreate)
|
||||
api.PUT("/snapshots/:name", apiSnapshotsUpdate)
|
||||
api.GET("/snapshots/:name", apiSnapshotsShow)
|
||||
api.GET("/snapshots/:name/packages", apiSnapshotsSearchPackages)
|
||||
api.DELETE("/snapshots/:name", apiSnapshotsDrop)
|
||||
api.GET("/snapshots/:name/diff/:withSnapshot", apiSnapshotsDiff)
|
||||
api.POST("/snapshots/:name/merge", apiSnapshotsMerge)
|
||||
api.POST("/snapshots/:name/pull", apiSnapshotsPull)
|
||||
authorize.GET("/snapshots", apiSnapshotsList)
|
||||
authorize.POST("/snapshots", apiSnapshotsCreate)
|
||||
authorize.PUT("/snapshots/:name", apiSnapshotsUpdate)
|
||||
authorize.GET("/snapshots/:name", apiSnapshotsShow)
|
||||
authorize.GET("/snapshots/:name/packages", apiSnapshotsSearchPackages)
|
||||
authorize.DELETE("/snapshots/:name", apiSnapshotsDrop)
|
||||
authorize.GET("/snapshots/:name/diff/:withSnapshot", apiSnapshotsDiff)
|
||||
authorize.POST("/snapshots/:name/merge", apiSnapshotsMerge)
|
||||
authorize.POST("/snapshots/:name/pull", apiSnapshotsPull)
|
||||
}
|
||||
|
||||
{
|
||||
api.GET("/packages/:key", apiPackagesShow)
|
||||
api.GET("/packages", apiPackages)
|
||||
authorize.GET("/packages/:key", apiPackagesShow)
|
||||
authorize.GET("/packages", apiPackages)
|
||||
}
|
||||
|
||||
{
|
||||
api.GET("/graph.:ext", apiGraph)
|
||||
authorize.GET("/graph.:ext", apiGraph)
|
||||
}
|
||||
{
|
||||
api.POST("/db/cleanup", apiDBCleanup)
|
||||
authorize.POST("/db/cleanup", apiDbCleanup)
|
||||
}
|
||||
{
|
||||
api.GET("/tasks", apiTasksList)
|
||||
api.POST("/tasks-clear", apiTasksClear)
|
||||
api.GET("/tasks-wait", apiTasksWait)
|
||||
api.GET("/tasks/:id/wait", apiTasksWaitForTaskByID)
|
||||
api.GET("/tasks/:id/output", apiTasksOutputShow)
|
||||
api.GET("/tasks/:id/detail", apiTasksDetailShow)
|
||||
api.GET("/tasks/:id/return_value", apiTasksReturnValueShow)
|
||||
api.GET("/tasks/:id", apiTasksShow)
|
||||
api.DELETE("/tasks/:id", apiTasksDelete)
|
||||
authorize.GET("/tasks", apiTasksList)
|
||||
authorize.POST("/tasks-clear", apiTasksClear)
|
||||
authorize.GET("/tasks-wait", apiTasksWait)
|
||||
authorize.GET("/tasks/:id/wait", apiTasksWaitForTaskByID)
|
||||
authorize.GET("/tasks/:id/output", apiTasksOutputShow)
|
||||
authorize.GET("/tasks/:id/detail", apiTasksDetailShow)
|
||||
authorize.GET("/tasks/:id/return_value", apiTasksReturnValueShow)
|
||||
authorize.GET("/tasks/:id", apiTasksShow)
|
||||
authorize.DELETE("/tasks/:id", apiTasksDelete)
|
||||
}
|
||||
|
||||
return router
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type RouterSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&RouterSuite{})
|
||||
|
||||
func (s *RouterSuite) TestRedirectSwagger(c *C) {
|
||||
// Test redirect from /docs to /docs/index.html
|
||||
response, _ := s.HTTPRequest("GET", "/docs", nil)
|
||||
c.Check(response.Code, Equals, 301)
|
||||
c.Check(response.Header().Get("Location"), Equals, "/docs/")
|
||||
}
|
||||
@@ -14,9 +14,7 @@ import (
|
||||
// @Router /api/s3 [get]
|
||||
func apiS3List(c *gin.Context) {
|
||||
keys := []string{}
|
||||
// Use safe accessor to get a copy of the map
|
||||
s3Roots := context.Config().GetS3PublishRoots()
|
||||
for k := range s3Roots {
|
||||
for k := range context.Config().S3PublishRoots {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
c.JSON(200, keys)
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type S3Suite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&S3Suite{})
|
||||
|
||||
func (s *S3Suite) TestS3List(c *C) {
|
||||
// Test listing S3 endpoints
|
||||
response, _ := s.HTTPRequest("GET", "/api/s3", nil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
c.Check(response.Header().Get("Content-Type"), Equals, "application/json; charset=utf-8")
|
||||
}
|
||||
+10
-4
@@ -33,7 +33,7 @@ func apiSnapshotsList(c *gin.Context) {
|
||||
}
|
||||
|
||||
result := []*deb.Snapshot{}
|
||||
_ = collection.ForEachSorted(SortMethodString, func(snapshot *deb.Snapshot) error {
|
||||
collection.ForEachSorted(SortMethodString, func(snapshot *deb.Snapshot) error {
|
||||
result = append(result, snapshot)
|
||||
return nil
|
||||
})
|
||||
@@ -251,6 +251,12 @@ func apiSnapshotsCreateFromRepository(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
err = CheckGroup(c, repo.LdapGroup)
|
||||
if err != nil {
|
||||
c.AbortWithError(403, err)
|
||||
return
|
||||
}
|
||||
|
||||
// including snapshot resource key
|
||||
resources := []string{string(repo.Key()), "S" + b.Name}
|
||||
taskName := fmt.Sprintf("Create snapshot of repo %s", name)
|
||||
@@ -555,7 +561,7 @@ func apiSnapshotsMerge(c *gin.Context) {
|
||||
}
|
||||
|
||||
if len(body.Sources) < 1 {
|
||||
AbortWithJSONError(c, http.StatusBadRequest, fmt.Errorf("minimum one source snapshot is required"))
|
||||
AbortWithJSONError(c, http.StatusBadRequest, fmt.Errorf("At least one source snapshot is required"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -765,7 +771,7 @@ func apiSnapshotsPull(c *gin.Context) {
|
||||
addedPackages := []string{}
|
||||
alreadySeen := map[string]bool{}
|
||||
|
||||
_ = destinationPackageList.ForEachIndexed(func(pkg *deb.Package) error {
|
||||
destinationPackageList.ForEachIndexed(func(pkg *deb.Package) error {
|
||||
key := pkg.Architecture + "_" + pkg.Name
|
||||
_, seen := alreadySeen[key]
|
||||
|
||||
@@ -781,7 +787,7 @@ func apiSnapshotsPull(c *gin.Context) {
|
||||
|
||||
// If !allMatches, add only first matching name-arch package
|
||||
if !seen || allMatches {
|
||||
_ = toPackageList.Add(pkg)
|
||||
toPackageList.Add(pkg)
|
||||
addedPackages = append(addedPackages, pkg.String())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,496 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type SnapshotAPITestSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&SnapshotAPITestSuite{})
|
||||
|
||||
func (s *SnapshotAPITestSuite) SetUpTest(c *C) {
|
||||
s.APISuite.SetUpTest(c)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestSnapshotShow(c *C) {
|
||||
// Test showing a specific snapshot
|
||||
req, _ := http.NewRequest("GET", "/api/snapshots/test-snapshot", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return 404 as the snapshot doesn't exist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestSnapshotUpdate(c *C) {
|
||||
// Test updating a snapshot
|
||||
params := struct {
|
||||
Name string `json:"Name"`
|
||||
Description string `json:"Description"`
|
||||
}{
|
||||
Name: "updated-snapshot",
|
||||
Description: "Updated description",
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("PUT", "/api/snapshots/test-snapshot", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return 404 as the snapshot doesn't exist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestSnapshotDrop(c *C) {
|
||||
// Test dropping a snapshot
|
||||
req, _ := http.NewRequest("DELETE", "/api/snapshots/test-snapshot", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return 404 as the snapshot doesn't exist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestSnapshotCreateFromRepository(c *C) {
|
||||
// Test creating a snapshot from repository
|
||||
params := struct {
|
||||
Name string `json:"Name"`
|
||||
Description string `json:"Description"`
|
||||
}{
|
||||
Name: "new-snapshot",
|
||||
Description: "Test snapshot",
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/repos/test-repo/snapshots", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return 404 as the repo doesn't exist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestSnapshotDiff(c *C) {
|
||||
// Test diffing two snapshots
|
||||
req, _ := http.NewRequest("GET", "/api/snapshots/snap1/diff/snap2", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return 404 as the snapshots don't exist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestSnapshotSearchPackages(c *C) {
|
||||
// Test searching packages in snapshot
|
||||
req, _ := http.NewRequest("GET", "/api/snapshots/test-snapshot/packages?q=Name", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return 404 as the snapshot doesn't exist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestSnapshotMerge(c *C) {
|
||||
// Test merging snapshots
|
||||
params := struct {
|
||||
Destination string `json:"Destination"`
|
||||
Sources []string `json:"Sources"`
|
||||
}{
|
||||
Destination: "merged-snapshot",
|
||||
Sources: []string{"snap1", "snap2"},
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/snapshots/merge", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return error as snapshots don't exist
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestSnapshotPull(c *C) {
|
||||
// Test pulling packages between snapshots
|
||||
params := struct {
|
||||
Source string `json:"Source"`
|
||||
Destination string `json:"Destination"`
|
||||
Queries []string `json:"Queries"`
|
||||
}{
|
||||
Source: "source-snap",
|
||||
Destination: "dest-snap",
|
||||
Queries: []string{"Name (~ nginx)"},
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/snapshots/pull", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return error as snapshots don't exist
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestSnapshotCreateFromMirror(c *C) {
|
||||
// Test creating snapshot from mirror
|
||||
params := struct {
|
||||
Name string `json:"Name"`
|
||||
Description string `json:"Description"`
|
||||
}{
|
||||
Name: "mirror-snapshot",
|
||||
Description: "Snapshot from mirror",
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/mirrors/test-mirror/snapshots", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return 404 as the mirror doesn't exist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsListGet(c *C) {
|
||||
// Test GET /api/snapshots endpoint
|
||||
req, _ := http.NewRequest("GET", "/api/snapshots", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle the request without crashing (will likely error due to no context)
|
||||
c.Check(w.Code, Not(Equals), 0)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsListWithSort(c *C) {
|
||||
// Test GET /api/snapshots with sort parameter
|
||||
req, _ := http.NewRequest("GET", "/api/snapshots?sort=name", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Not(Equals), 0)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsListWithDifferentSorts(c *C) {
|
||||
// Test various sort methods
|
||||
sortMethods := []string{"name", "time", "created"}
|
||||
|
||||
for _, sortMethod := range sortMethods {
|
||||
req, _ := http.NewRequest("GET", "/api/snapshots?sort="+sortMethod, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Sort method: %s", sortMethod))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsCreatePost(c *C) {
|
||||
// Test POST /api/snapshots endpoint
|
||||
requestBody := snapshotsCreateParams{
|
||||
Name: "test-snapshot",
|
||||
Description: "Test snapshot",
|
||||
SourceSnapshots: []string{"source1"},
|
||||
PackageRefs: []string{},
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(requestBody)
|
||||
req, _ := http.NewRequest("POST", "/api/snapshots", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle the request without crashing (will likely error due to no context)
|
||||
c.Check(w.Code, Not(Equals), 0)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsCreateInvalidJSON(c *C) {
|
||||
// Test POST with invalid JSON
|
||||
req, _ := http.NewRequest("POST", "/api/snapshots", strings.NewReader("invalid json"))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 400) // Should return bad request for invalid JSON
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsCreateMissingName(c *C) {
|
||||
// Test POST with missing required name field
|
||||
requestBody := map[string]interface{}{
|
||||
"Description": "Test without name",
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(requestBody)
|
||||
req, _ := http.NewRequest("POST", "/api/snapshots", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 400) // Should return bad request for missing name
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsCreateFromMirrorPost(c *C) {
|
||||
// Test POST /api/mirrors/{name}/snapshots endpoint
|
||||
requestBody := snapshotsCreateFromMirrorParams{
|
||||
Name: "mirror-snapshot",
|
||||
Description: "Snapshot from mirror",
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(requestBody)
|
||||
req, _ := http.NewRequest("POST", "/api/mirrors/test-mirror/snapshots", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle the request without crashing (will likely error due to no context)
|
||||
c.Check(w.Code, Not(Equals), 0)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsCreateFromMirrorInvalidJSON(c *C) {
|
||||
// Test POST with invalid JSON for mirror snapshot
|
||||
req, _ := http.NewRequest("POST", "/api/mirrors/test-mirror/snapshots", strings.NewReader("invalid json"))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 400) // Should return bad request for invalid JSON
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsCreateFromMirrorMissingName(c *C) {
|
||||
// Test POST with missing required name field for mirror snapshot
|
||||
requestBody := map[string]interface{}{
|
||||
"Description": "Mirror snapshot without name",
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(requestBody)
|
||||
req, _ := http.NewRequest("POST", "/api/mirrors/test-mirror/snapshots", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 400) // Should return bad request for missing name
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsCreateWithAsync(c *C) {
|
||||
// Test POST with async parameter
|
||||
requestBody := snapshotsCreateParams{
|
||||
Name: "async-snapshot",
|
||||
Description: "Async test snapshot",
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(requestBody)
|
||||
req, _ := http.NewRequest("POST", "/api/snapshots?_async=true", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Not(Equals), 0)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsCreateFromMirrorWithAsync(c *C) {
|
||||
// Test POST mirror snapshot with async parameter
|
||||
requestBody := snapshotsCreateFromMirrorParams{
|
||||
Name: "async-mirror-snapshot",
|
||||
Description: "Async mirror snapshot",
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(requestBody)
|
||||
req, _ := http.NewRequest("POST", "/api/mirrors/test-mirror/snapshots?_async=true", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Not(Equals), 0)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestSnapshotsCreateParamsStruct(c *C) {
|
||||
// Test snapshotsCreateParams struct
|
||||
params := snapshotsCreateParams{
|
||||
Name: "test-name",
|
||||
Description: "test-description",
|
||||
SourceSnapshots: []string{"snap1", "snap2"},
|
||||
PackageRefs: []string{"ref1", "ref2"},
|
||||
}
|
||||
|
||||
c.Check(params.Name, Equals, "test-name")
|
||||
c.Check(params.Description, Equals, "test-description")
|
||||
c.Check(params.SourceSnapshots, DeepEquals, []string{"snap1", "snap2"})
|
||||
c.Check(params.PackageRefs, DeepEquals, []string{"ref1", "ref2"})
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestSnapshotsCreateFromMirrorParamsStruct(c *C) {
|
||||
// Test snapshotsCreateFromMirrorParams struct
|
||||
params := snapshotsCreateFromMirrorParams{
|
||||
Name: "mirror-test-name",
|
||||
Description: "mirror-test-description",
|
||||
}
|
||||
|
||||
c.Check(params.Name, Equals, "mirror-test-name")
|
||||
c.Check(params.Description, Equals, "mirror-test-description")
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsCreateEmptyRequest(c *C) {
|
||||
// Test POST with empty request body
|
||||
req, _ := http.NewRequest("POST", "/api/snapshots", strings.NewReader(""))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 400) // Should return bad request for empty body
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsCreateFromMirrorEmptyRequest(c *C) {
|
||||
// Test POST mirror snapshot with empty request body
|
||||
req, _ := http.NewRequest("POST", "/api/mirrors/test-mirror/snapshots", strings.NewReader(""))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 400) // Should return bad request for empty body
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsListDefaultSort(c *C) {
|
||||
// Test that default sort is applied when no sort parameter provided
|
||||
req, _ := http.NewRequest("GET", "/api/snapshots", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Endpoint should handle default sort without issues
|
||||
c.Check(w.Code, Not(Equals), 0)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsCreateComplexPayload(c *C) {
|
||||
// Test POST with complex payload including all fields
|
||||
requestBody := snapshotsCreateParams{
|
||||
Name: "complex-snapshot",
|
||||
Description: "Complex test snapshot with multiple sources",
|
||||
SourceSnapshots: []string{"base-snapshot", "updates-snapshot", "security-snapshot"},
|
||||
PackageRefs: []string{"pkg1_1.0_amd64", "pkg2_2.0_i386", "pkg3_3.0_all"},
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(requestBody)
|
||||
req, _ := http.NewRequest("POST", "/api/snapshots", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Not(Equals), 0)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsHTTPMethods(c *C) {
|
||||
// Test that only allowed HTTP methods work
|
||||
|
||||
// Test unsupported methods for snapshots list
|
||||
deniedMethods := []string{"PUT", "DELETE", "PATCH"}
|
||||
for _, method := range deniedMethods {
|
||||
req, _ := http.NewRequest(method, "/api/snapshots", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 404, Commentf("Method %s should not be allowed for snapshots list", method))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsCreateSpecialCharacters(c *C) {
|
||||
// Test snapshot creation with special characters in names
|
||||
specialNames := []string{
|
||||
"snapshot-with-dashes",
|
||||
"snapshot_with_underscores",
|
||||
"snapshot.with.dots",
|
||||
"snapshot123",
|
||||
"UPPERCASESNAPSHOT",
|
||||
}
|
||||
|
||||
for _, name := range specialNames {
|
||||
requestBody := snapshotsCreateParams{
|
||||
Name: name,
|
||||
Description: "Test snapshot with special characters",
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(requestBody)
|
||||
req, _ := http.NewRequest("POST", "/api/snapshots", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Special name test failed: %s", name))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsListEmptyResponse(c *C) {
|
||||
// Test snapshots list when no snapshots exist
|
||||
req, _ := http.NewRequest("GET", "/api/snapshots", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should return some response (likely error due to no context, but shouldn't crash)
|
||||
c.Check(w.Code, Not(Equals), 0)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsCreateWithoutContentType(c *C) {
|
||||
// Test POST without Content-Type header
|
||||
requestBody := `{"Name": "test-snapshot"}`
|
||||
req, _ := http.NewRequest("POST", "/api/snapshots", strings.NewReader(requestBody))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle missing content type
|
||||
c.Check(w.Code, Not(Equals), 0)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsParameterEdgeCases(c *C) {
|
||||
// Test edge cases for parameter validation
|
||||
|
||||
// Test with very long name
|
||||
longName := strings.Repeat("a", 1000)
|
||||
requestBody := snapshotsCreateParams{
|
||||
Name: longName,
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(requestBody)
|
||||
req, _ := http.NewRequest("POST", "/api/snapshots", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Not(Equals), 0)
|
||||
|
||||
// Test with empty arrays
|
||||
emptyArrayBody := snapshotsCreateParams{
|
||||
Name: "empty-arrays",
|
||||
SourceSnapshots: []string{},
|
||||
PackageRefs: []string{},
|
||||
}
|
||||
|
||||
jsonBody, _ = json.Marshal(emptyArrayBody)
|
||||
req, _ = http.NewRequest("POST", "/api/snapshots", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Not(Equals), 0)
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type StorageTestSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&StorageTestSuite{})
|
||||
|
||||
func (s *StorageTestSuite) SetUpTest(c *C) {
|
||||
s.APISuite.SetUpTest(c)
|
||||
}
|
||||
|
||||
func (s *StorageTestSuite) TestStorageListStructure(c *C) {
|
||||
// Test storage list endpoint structure
|
||||
req, _ := http.NewRequest("GET", "/api/storage", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
c.Check(w.Header().Get("Content-Type"), Equals, "application/json; charset=utf-8")
|
||||
|
||||
// Should return some storage information without error
|
||||
}
|
||||
|
||||
func (s *StorageTestSuite) TestStorageHTTPMethods(c *C) {
|
||||
// Test that only GET method is allowed
|
||||
deniedMethods := []string{"POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"}
|
||||
|
||||
for _, method := range deniedMethods {
|
||||
req, _ := http.NewRequest(method, "/api/storage", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 404, Commentf("Method: %s should be denied", method))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StorageTestSuite) TestStorageEndpointReliability(c *C) {
|
||||
// Test multiple calls to ensure endpoint is reliable
|
||||
for i := 0; i < 5; i++ {
|
||||
req, _ := http.NewRequest("GET", "/api/storage", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 200, Commentf("Call #%d", i+1))
|
||||
c.Check(w.Header().Get("Content-Type"), Equals, "application/json; charset=utf-8")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StorageTestSuite) TestStorageResponseStructure(c *C) {
|
||||
// Test that response structure is consistent
|
||||
req, _ := http.NewRequest("GET", "/api/storage", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
|
||||
// Should have valid JSON response
|
||||
body := w.Body.String()
|
||||
c.Check(len(body), Not(Equals), 0)
|
||||
|
||||
// Should start with valid JSON structure
|
||||
c.Check(body[0], Equals, byte('{'), Commentf("Response should be JSON object"))
|
||||
}
|
||||
@@ -1,449 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type TaskTestSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&TaskTestSuite{})
|
||||
|
||||
func (s *TaskTestSuite) SetUpTest(c *C) {
|
||||
s.APISuite.SetUpTest(c)
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksListEmpty(c *C) {
|
||||
// Test listing tasks when none exist
|
||||
req, _ := http.NewRequest("GET", "/api/tasks", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
c.Check(w.Header().Get("Content-Type"), Equals, "application/json; charset=utf-8")
|
||||
|
||||
// Will likely return empty array due to no context, but tests structure
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksClearStructure(c *C) {
|
||||
// Test clearing tasks
|
||||
req, _ := http.NewRequest("POST", "/api/tasks-clear", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
c.Check(w.Header().Get("Content-Type"), Equals, "application/json; charset=utf-8")
|
||||
|
||||
// Should return empty object
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksWaitStructure(c *C) {
|
||||
// Test waiting for all tasks
|
||||
req, _ := http.NewRequest("GET", "/api/tasks-wait", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
c.Check(w.Header().Get("Content-Type"), Equals, "application/json; charset=utf-8")
|
||||
|
||||
// Should return empty object after waiting
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksWaitForTaskByIDStructure(c *C) {
|
||||
// Test waiting for specific task by ID
|
||||
req, _ := http.NewRequest("GET", "/api/tasks/123/wait", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context or invalid task, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksWaitForTaskByIDInvalidID(c *C) {
|
||||
// Test waiting for task with invalid ID
|
||||
invalidIDs := []string{"invalid", "abc", "123.45"} // removed empty string as it causes redirect
|
||||
|
||||
for _, id := range invalidIDs {
|
||||
req, _ := http.NewRequest("GET", "/api/tasks/"+id+"/wait", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should return 500 for invalid ID format
|
||||
c.Check(w.Code, Equals, 500, Commentf("ID: %s", id))
|
||||
}
|
||||
|
||||
// Test negative ID separately - it's a valid int but invalid task ID
|
||||
req, _ := http.NewRequest("GET", "/api/tasks/-1/wait", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Check(w.Code, Equals, 400, Commentf("ID: -1 should return 400 (not found)"))
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksShowStructure(c *C) {
|
||||
// Test showing specific task by ID
|
||||
req, _ := http.NewRequest("GET", "/api/tasks/123", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context or invalid task, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksShowInvalidID(c *C) {
|
||||
// Test showing task with invalid ID
|
||||
invalidIDs := []string{"invalid", "abc", "123.45"} // removed empty string as it causes redirect
|
||||
|
||||
for _, id := range invalidIDs {
|
||||
req, _ := http.NewRequest("GET", "/api/tasks/"+id, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should return 500 for invalid ID format
|
||||
c.Check(w.Code, Equals, 500, Commentf("ID: %s", id))
|
||||
}
|
||||
|
||||
// Test negative ID separately - it's a valid int but invalid task ID
|
||||
req, _ := http.NewRequest("GET", "/api/tasks/-1", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Check(w.Code, Equals, 404, Commentf("ID: -1 should return 404 (not found)"))
|
||||
|
||||
// Test very large number separately - causes int overflow
|
||||
req, _ = http.NewRequest("GET", "/api/tasks/999999999999999999999", nil)
|
||||
w = httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Check(w.Code, Equals, 500, Commentf("Very large number should return 500"))
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksOutputStructure(c *C) {
|
||||
// Test getting task output by ID
|
||||
req, _ := http.NewRequest("GET", "/api/tasks/123/output", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context or invalid task, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksOutputInvalidID(c *C) {
|
||||
// Test getting task output with invalid ID
|
||||
invalidIDs := []string{"invalid", "abc", "123.45"} // removed empty string as it causes redirect
|
||||
|
||||
for _, id := range invalidIDs {
|
||||
req, _ := http.NewRequest("GET", "/api/tasks/"+id+"/output", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should return 500 for invalid ID format
|
||||
c.Check(w.Code, Equals, 500, Commentf("ID: %s", id))
|
||||
}
|
||||
|
||||
// Test negative ID separately - it's a valid int but invalid task ID
|
||||
req, _ := http.NewRequest("GET", "/api/tasks/-1/output", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Check(w.Code, Equals, 404, Commentf("ID: -1 should return 404 (not found)"))
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksDetailStructure(c *C) {
|
||||
// Test getting task detail by ID
|
||||
req, _ := http.NewRequest("GET", "/api/tasks/123/detail", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context or invalid task, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksDetailInvalidID(c *C) {
|
||||
// Test getting task detail with invalid ID
|
||||
invalidIDs := []string{"invalid", "abc", "123.45"} // removed empty string as it causes redirect
|
||||
|
||||
for _, id := range invalidIDs {
|
||||
req, _ := http.NewRequest("GET", "/api/tasks/"+id+"/detail", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should return 500 for invalid ID format
|
||||
c.Check(w.Code, Equals, 500, Commentf("ID: %s", id))
|
||||
}
|
||||
|
||||
// Test negative ID separately - it's a valid int but invalid task ID
|
||||
req, _ := http.NewRequest("GET", "/api/tasks/-1/detail", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Check(w.Code, Equals, 404, Commentf("ID: -1 should return 404 (not found)"))
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksReturnValueStructure(c *C) {
|
||||
// Test getting task return value by ID
|
||||
req, _ := http.NewRequest("GET", "/api/tasks/123/return_value", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context or invalid task, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksReturnValueInvalidID(c *C) {
|
||||
// Test getting task return value with invalid ID
|
||||
invalidIDs := []string{"invalid", "abc", "123.45"} // removed empty string as it causes redirect
|
||||
|
||||
for _, id := range invalidIDs {
|
||||
req, _ := http.NewRequest("GET", "/api/tasks/"+id+"/return_value", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should return 500 for invalid ID format
|
||||
c.Check(w.Code, Equals, 500, Commentf("ID: %s", id))
|
||||
}
|
||||
|
||||
// Test negative ID separately - it's a valid int but invalid task ID
|
||||
req, _ := http.NewRequest("GET", "/api/tasks/-1/return_value", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Check(w.Code, Equals, 404, Commentf("ID: -1 should return 404 (not found)"))
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksDeleteStructure(c *C) {
|
||||
// Test deleting task by ID
|
||||
req, _ := http.NewRequest("DELETE", "/api/tasks/123", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context or invalid task, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksDeleteInvalidID(c *C) {
|
||||
// Test deleting task with invalid ID
|
||||
invalidIDs := []string{"invalid", "abc", "123.45"} // removed empty string as it causes redirect
|
||||
|
||||
for _, id := range invalidIDs {
|
||||
req, _ := http.NewRequest("DELETE", "/api/tasks/"+id, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should return 500 for invalid ID format
|
||||
c.Check(w.Code, Equals, 500, Commentf("ID: %s", id))
|
||||
}
|
||||
|
||||
// Test negative ID separately - it's a valid int but invalid task ID
|
||||
req, _ := http.NewRequest("DELETE", "/api/tasks/-1", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Check(w.Code, Equals, 400, Commentf("ID: -1 should return 400 (not found)"))
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksValidIDFormats(c *C) {
|
||||
// Test various valid ID formats
|
||||
validIDs := []string{"0", "1", "123", "999", "2147483647"} // Max int32
|
||||
|
||||
for _, id := range validIDs {
|
||||
// Test show endpoint
|
||||
req, _ := http.NewRequest("GET", "/api/tasks/"+id, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should not be 500 (invalid format), might be 404 (not found) or other error
|
||||
c.Check(w.Code, Not(Equals), 500, Commentf("ID: %s", id))
|
||||
|
||||
// Test wait endpoint
|
||||
req, _ = http.NewRequest("GET", "/api/tasks/"+id+"/wait", nil)
|
||||
w = httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should not be 500 (invalid format)
|
||||
c.Check(w.Code, Not(Equals), 500, Commentf("ID: %s", id))
|
||||
|
||||
// Test output endpoint
|
||||
req, _ = http.NewRequest("GET", "/api/tasks/"+id+"/output", nil)
|
||||
w = httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should not be 500 (invalid format)
|
||||
c.Check(w.Code, Not(Equals), 500, Commentf("ID: %s", id))
|
||||
|
||||
// Test detail endpoint
|
||||
req, _ = http.NewRequest("GET", "/api/tasks/"+id+"/detail", nil)
|
||||
w = httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should not be 500 (invalid format)
|
||||
c.Check(w.Code, Not(Equals), 500, Commentf("ID: %s", id))
|
||||
|
||||
// Test return_value endpoint
|
||||
req, _ = http.NewRequest("GET", "/api/tasks/"+id+"/return_value", nil)
|
||||
w = httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should not be 500 (invalid format)
|
||||
c.Check(w.Code, Not(Equals), 500, Commentf("ID: %s", id))
|
||||
|
||||
// Test delete endpoint
|
||||
req, _ = http.NewRequest("DELETE", "/api/tasks/"+id, nil)
|
||||
w = httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should not be 500 (invalid format)
|
||||
c.Check(w.Code, Not(Equals), 500, Commentf("ID: %s", id))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksParameterEdgeCases(c *C) {
|
||||
// Test edge cases in parameter handling
|
||||
edgeCases := []struct {
|
||||
path string
|
||||
description string
|
||||
}{
|
||||
{"/api/tasks/0", "zero ID"},
|
||||
{"/api/tasks/1", "single digit ID"},
|
||||
{"/api/tasks/2147483647", "max int32 ID"},
|
||||
{"/api/tasks/00123", "leading zeros"},
|
||||
{"/api/tasks/+123", "positive sign"},
|
||||
}
|
||||
|
||||
for _, tc := range edgeCases {
|
||||
req, _ := http.NewRequest("GET", tc.path, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle edge cases gracefully without crashing
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Test: %s", tc.description))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksHTTPMethods(c *C) {
|
||||
// Test that correct HTTP methods are supported for each endpoint
|
||||
methodTests := []struct {
|
||||
path string
|
||||
allowedMethods []string
|
||||
deniedMethods []string
|
||||
}{
|
||||
{"/api/tasks", []string{"GET"}, []string{"POST", "PUT", "DELETE", "PATCH"}},
|
||||
{"/api/tasks-clear", []string{"POST"}, []string{"GET", "PUT", "DELETE", "PATCH"}},
|
||||
{"/api/tasks-wait", []string{"GET"}, []string{"POST", "PUT", "DELETE", "PATCH"}},
|
||||
{"/api/tasks/123", []string{"GET", "DELETE"}, []string{"POST", "PUT", "PATCH"}},
|
||||
{"/api/tasks/123/wait", []string{"GET"}, []string{"POST", "PUT", "DELETE", "PATCH"}},
|
||||
{"/api/tasks/123/output", []string{"GET"}, []string{"POST", "PUT", "DELETE", "PATCH"}},
|
||||
{"/api/tasks/123/detail", []string{"GET"}, []string{"POST", "PUT", "DELETE", "PATCH"}},
|
||||
{"/api/tasks/123/return_value", []string{"GET"}, []string{"POST", "PUT", "DELETE", "PATCH"}},
|
||||
}
|
||||
|
||||
for _, test := range methodTests {
|
||||
// Test denied methods return 404 (method not allowed for route)
|
||||
for _, method := range test.deniedMethods {
|
||||
req, _ := http.NewRequest(method, test.path, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 404, Commentf("Path: %s, Method: %s", test.path, method))
|
||||
}
|
||||
|
||||
// Test allowed methods are handled (may return errors but not method not allowed)
|
||||
for _, method := range test.allowedMethods {
|
||||
req, _ := http.NewRequest(method, test.path, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle the request (200, 400, 404 for not found are OK)
|
||||
// Just ensure it's not 0 (no response) or 405 (method not allowed)
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Path: %s, Method: %s", test.path, method))
|
||||
c.Check(w.Code, Not(Equals), 405, Commentf("Path: %s, Method: %s", test.path, method))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksContentTypes(c *C) {
|
||||
// Test content type handling for different endpoints
|
||||
contentTypeTests := []struct {
|
||||
path string
|
||||
method string
|
||||
expectedType string
|
||||
}{
|
||||
{"/api/tasks", "GET", "application/json"},
|
||||
{"/api/tasks-clear", "POST", "application/json"},
|
||||
{"/api/tasks-wait", "GET", "application/json"},
|
||||
{"/api/tasks/123", "GET", "application/json"},
|
||||
{"/api/tasks/123/wait", "GET", "application/json"},
|
||||
{"/api/tasks/123/output", "GET", ""}, // Text content
|
||||
{"/api/tasks/123/detail", "GET", "application/json"},
|
||||
{"/api/tasks/123/return_value", "GET", "application/json"},
|
||||
}
|
||||
|
||||
for _, test := range contentTypeTests {
|
||||
req, _ := http.NewRequest(test.method, test.path, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
if test.expectedType != "" {
|
||||
// Check that JSON endpoints return JSON content type
|
||||
contentType := w.Header().Get("Content-Type")
|
||||
c.Check(contentType, Matches, ".*"+test.expectedType+".*",
|
||||
Commentf("Path: %s, Expected: %s, Got: %s", test.path, test.expectedType, contentType))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksErrorConditions(c *C) {
|
||||
// Test various error conditions
|
||||
errorTests := []struct {
|
||||
description string
|
||||
path string
|
||||
method string
|
||||
expectedErr bool
|
||||
}{
|
||||
{"Non-existent task ID", "/api/tasks/999999", "GET", true},
|
||||
{"Non-existent task wait", "/api/tasks/999999/wait", "GET", true},
|
||||
{"Non-existent task output", "/api/tasks/999999/output", "GET", true},
|
||||
{"Non-existent task detail", "/api/tasks/999999/detail", "GET", true},
|
||||
{"Non-existent task return value", "/api/tasks/999999/return_value", "GET", true},
|
||||
{"Non-existent task delete", "/api/tasks/999999", "DELETE", true},
|
||||
{"Tasks list endpoint", "/api/tasks", "GET", true}, // Valid endpoint
|
||||
{"Extra path segments", "/api/tasks/123/extra/segment", "GET", false}, // Route not matched
|
||||
}
|
||||
|
||||
for _, test := range errorTests {
|
||||
req, _ := http.NewRequest(test.method, test.path, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// All should return some response without crashing
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Test: %s", test.description))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksResourceManagement(c *C) {
|
||||
// Test that endpoints handle resource management correctly
|
||||
endpoints := []string{
|
||||
"/api/tasks",
|
||||
"/api/tasks-clear",
|
||||
"/api/tasks-wait",
|
||||
"/api/tasks/1",
|
||||
"/api/tasks/1/wait",
|
||||
"/api/tasks/1/output",
|
||||
"/api/tasks/1/detail",
|
||||
"/api/tasks/1/return_value",
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
method := "GET"
|
||||
if endpoint == "/api/tasks-clear" {
|
||||
method = "POST"
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest(method, endpoint, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should complete without hanging or crashing
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Endpoint: %s", endpoint))
|
||||
|
||||
// Response should have proper headers
|
||||
c.Check(w.Header(), NotNil, Commentf("Endpoint: %s", endpoint))
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"rootDir": "~/.aptly",
|
||||
"downloadConcurrency": 4,
|
||||
"downloadSpeedLimit": 0,
|
||||
"databaseOpenAttempts": 10,
|
||||
"architectures": ["amd64", "i386", "arm64"],
|
||||
"dependencyFollowSuggests": false,
|
||||
"dependencyFollowRecommends": false,
|
||||
"dependencyFollowAllVariants": false,
|
||||
"dependencyFollowSource": false,
|
||||
"gpgDisableSign": false,
|
||||
"gpgDisableVerify": false,
|
||||
"downloadSourcePackages": false,
|
||||
"ppaDistributorID": "ubuntu",
|
||||
"ppaCodename": "",
|
||||
"s3ConcurrentUploads": 4,
|
||||
"s3UploadQueueSize": 1000,
|
||||
"databaseBackend": {
|
||||
"type": "etcd",
|
||||
"url": "localhost:2379",
|
||||
"timeout": "120s",
|
||||
"writeRetries": 3,
|
||||
"writeQueue": {
|
||||
"enabled": true,
|
||||
"queueSize": 1000,
|
||||
"maxWritesPerSec": 100,
|
||||
"batchMaxSize": 50,
|
||||
"batchMaxWaitMs": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,716 +0,0 @@
|
||||
package aptly
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
// Launch gocheck tests
|
||||
func Test(t *testing.T) {
|
||||
TestingT(t)
|
||||
}
|
||||
|
||||
type AptlySuite struct{}
|
||||
|
||||
var _ = Suite(&AptlySuite{})
|
||||
|
||||
// Mock implementations for testing interfaces
|
||||
|
||||
type MockPackagePool struct {
|
||||
verifyFunc func(string, string, *utils.ChecksumInfo, ChecksumStorage) (string, bool, error)
|
||||
importFunc func(string, string, *utils.ChecksumInfo, bool, ChecksumStorage) (string, error)
|
||||
legacyPathFunc func(string, *utils.ChecksumInfo) (string, error)
|
||||
sizeFunc func(string) (int64, error)
|
||||
openFunc func(string) (ReadSeekerCloser, error)
|
||||
filepathListFunc func(Progress) ([]string, error)
|
||||
removeFunc func(string) (int64, error)
|
||||
}
|
||||
|
||||
func (m *MockPackagePool) Verify(poolPath, basename string, checksums *utils.ChecksumInfo, storage ChecksumStorage) (string, bool, error) {
|
||||
if m.verifyFunc != nil {
|
||||
return m.verifyFunc(poolPath, basename, checksums, storage)
|
||||
}
|
||||
return poolPath, true, nil
|
||||
}
|
||||
|
||||
func (m *MockPackagePool) Import(srcPath, basename string, checksums *utils.ChecksumInfo, move bool, storage ChecksumStorage) (string, error) {
|
||||
if m.importFunc != nil {
|
||||
return m.importFunc(srcPath, basename, checksums, move, storage)
|
||||
}
|
||||
return "imported/path/" + basename, nil
|
||||
}
|
||||
|
||||
func (m *MockPackagePool) LegacyPath(filename string, checksums *utils.ChecksumInfo) (string, error) {
|
||||
if m.legacyPathFunc != nil {
|
||||
return m.legacyPathFunc(filename, checksums)
|
||||
}
|
||||
return "legacy/" + filename, nil
|
||||
}
|
||||
|
||||
func (m *MockPackagePool) Size(path string) (int64, error) {
|
||||
if m.sizeFunc != nil {
|
||||
return m.sizeFunc(path)
|
||||
}
|
||||
return 1024, nil
|
||||
}
|
||||
|
||||
func (m *MockPackagePool) Open(path string) (ReadSeekerCloser, error) {
|
||||
if m.openFunc != nil {
|
||||
return m.openFunc(path)
|
||||
}
|
||||
return &MockReadSeekerCloser{content: []byte("mock file content")}, nil
|
||||
}
|
||||
|
||||
func (m *MockPackagePool) FilepathList(progress Progress) ([]string, error) {
|
||||
if m.filepathListFunc != nil {
|
||||
return m.filepathListFunc(progress)
|
||||
}
|
||||
return []string{"file1.deb", "file2.deb"}, nil
|
||||
}
|
||||
|
||||
func (m *MockPackagePool) Remove(path string) (int64, error) {
|
||||
if m.removeFunc != nil {
|
||||
return m.removeFunc(path)
|
||||
}
|
||||
return 1024, nil
|
||||
}
|
||||
|
||||
type MockReadSeekerCloser struct {
|
||||
content []byte
|
||||
pos int64
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (m *MockReadSeekerCloser) Read(p []byte) (int, error) {
|
||||
if m.closed {
|
||||
return 0, errors.New("closed")
|
||||
}
|
||||
if m.pos >= int64(len(m.content)) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n := copy(p, m.content[m.pos:])
|
||||
m.pos += int64(n)
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (m *MockReadSeekerCloser) Seek(offset int64, whence int) (int64, error) {
|
||||
if m.closed {
|
||||
return 0, errors.New("closed")
|
||||
}
|
||||
switch whence {
|
||||
case io.SeekStart:
|
||||
m.pos = offset
|
||||
case io.SeekCurrent:
|
||||
m.pos += offset
|
||||
case io.SeekEnd:
|
||||
m.pos = int64(len(m.content)) + offset
|
||||
}
|
||||
if m.pos < 0 {
|
||||
m.pos = 0
|
||||
}
|
||||
if m.pos > int64(len(m.content)) {
|
||||
m.pos = int64(len(m.content))
|
||||
}
|
||||
return m.pos, nil
|
||||
}
|
||||
|
||||
func (m *MockReadSeekerCloser) Close() error {
|
||||
m.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
type MockPublishedStorage struct {
|
||||
mkDirFunc func(string) error
|
||||
putFileFunc func(string, string) error
|
||||
removeDirsFunc func(string, Progress) error
|
||||
removeFunc func(string) error
|
||||
linkFromPoolFunc func(string, string, string, PackagePool, string, utils.ChecksumInfo, bool) error
|
||||
filelistFunc func(string) ([]string, error)
|
||||
renameFileFunc func(string, string) error
|
||||
symLinkFunc func(string, string) error
|
||||
hardLinkFunc func(string, string) error
|
||||
fileExistsFunc func(string) (bool, error)
|
||||
readLinkFunc func(string) (string, error)
|
||||
}
|
||||
|
||||
func (m *MockPublishedStorage) MkDir(path string) error {
|
||||
if m.mkDirFunc != nil {
|
||||
return m.mkDirFunc(path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockPublishedStorage) PutFile(path, sourceFilename string) error {
|
||||
if m.putFileFunc != nil {
|
||||
return m.putFileFunc(path, sourceFilename)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockPublishedStorage) RemoveDirs(path string, progress Progress) error {
|
||||
if m.removeDirsFunc != nil {
|
||||
return m.removeDirsFunc(path, progress)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockPublishedStorage) Remove(path string) error {
|
||||
if m.removeFunc != nil {
|
||||
return m.removeFunc(path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockPublishedStorage) LinkFromPool(publishedPrefix, publishedRelPath, fileName string, sourcePool PackagePool, sourcePath string, sourceChecksums utils.ChecksumInfo, force bool) error {
|
||||
if m.linkFromPoolFunc != nil {
|
||||
return m.linkFromPoolFunc(publishedPrefix, publishedRelPath, fileName, sourcePool, sourcePath, sourceChecksums, force)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockPublishedStorage) Filelist(prefix string) ([]string, error) {
|
||||
if m.filelistFunc != nil {
|
||||
return m.filelistFunc(prefix)
|
||||
}
|
||||
return []string{"file1", "file2"}, nil
|
||||
}
|
||||
|
||||
func (m *MockPublishedStorage) RenameFile(oldName, newName string) error {
|
||||
if m.renameFileFunc != nil {
|
||||
return m.renameFileFunc(oldName, newName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockPublishedStorage) SymLink(src, dst string) error {
|
||||
if m.symLinkFunc != nil {
|
||||
return m.symLinkFunc(src, dst)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockPublishedStorage) HardLink(src, dst string) error {
|
||||
if m.hardLinkFunc != nil {
|
||||
return m.hardLinkFunc(src, dst)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockPublishedStorage) FileExists(path string) (bool, error) {
|
||||
if m.fileExistsFunc != nil {
|
||||
return m.fileExistsFunc(path)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (m *MockPublishedStorage) ReadLink(path string) (string, error) {
|
||||
if m.readLinkFunc != nil {
|
||||
return m.readLinkFunc(path)
|
||||
}
|
||||
return "target", nil
|
||||
}
|
||||
|
||||
func (m *MockPublishedStorage) Flush() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type MockProgress struct {
|
||||
buffer bytes.Buffer
|
||||
started bool
|
||||
barStarted bool
|
||||
barProgress int
|
||||
}
|
||||
|
||||
func (m *MockProgress) Write(p []byte) (n int, err error) {
|
||||
return m.buffer.Write(p)
|
||||
}
|
||||
|
||||
func (m *MockProgress) Start() {
|
||||
m.started = true
|
||||
}
|
||||
|
||||
func (m *MockProgress) Shutdown() {
|
||||
m.started = false
|
||||
}
|
||||
|
||||
func (m *MockProgress) Flush() {
|
||||
// Nothing to do in mock
|
||||
}
|
||||
|
||||
func (m *MockProgress) InitBar(count int64, isBytes bool, barType BarType) {
|
||||
m.barStarted = true
|
||||
}
|
||||
|
||||
func (m *MockProgress) ShutdownBar() {
|
||||
m.barStarted = false
|
||||
}
|
||||
|
||||
func (m *MockProgress) AddBar(count int) {
|
||||
m.barProgress += count
|
||||
}
|
||||
|
||||
func (m *MockProgress) SetBar(count int) {
|
||||
m.barProgress = count
|
||||
}
|
||||
|
||||
func (m *MockProgress) Printf(msg string, a ...interface{}) {
|
||||
fmt.Fprintf(&m.buffer, msg, a...)
|
||||
}
|
||||
|
||||
func (m *MockProgress) ColoredPrintf(msg string, a ...interface{}) {
|
||||
// Strip color codes for testing
|
||||
cleanMsg := strings.ReplaceAll(msg, "@r", "")
|
||||
cleanMsg = strings.ReplaceAll(cleanMsg, "@g", "")
|
||||
cleanMsg = strings.ReplaceAll(cleanMsg, "@y", "")
|
||||
cleanMsg = strings.ReplaceAll(cleanMsg, "@!", "")
|
||||
cleanMsg = strings.ReplaceAll(cleanMsg, "@|", "")
|
||||
fmt.Fprintf(&m.buffer, cleanMsg, a...)
|
||||
}
|
||||
|
||||
func (m *MockProgress) PrintfStdErr(msg string, a ...interface{}) {
|
||||
fmt.Fprintf(&m.buffer, "[STDERR] "+msg, a...)
|
||||
}
|
||||
|
||||
type MockDownloader struct {
|
||||
downloadFunc func(context.Context, string, string) error
|
||||
downloadWithChecksumFunc func(context.Context, string, string, *utils.ChecksumInfo, bool) error
|
||||
progress Progress
|
||||
getLengthFunc func(context.Context, string) (int64, error)
|
||||
}
|
||||
|
||||
func (m *MockDownloader) Download(ctx context.Context, url, destination string) error {
|
||||
if m.downloadFunc != nil {
|
||||
return m.downloadFunc(ctx, url, destination)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockDownloader) DownloadWithChecksum(ctx context.Context, url, destination string, expected *utils.ChecksumInfo, ignoreMismatch bool) error {
|
||||
if m.downloadWithChecksumFunc != nil {
|
||||
return m.downloadWithChecksumFunc(ctx, url, destination, expected, ignoreMismatch)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockDownloader) GetProgress() Progress {
|
||||
if m.progress != nil {
|
||||
return m.progress
|
||||
}
|
||||
return &MockProgress{}
|
||||
}
|
||||
|
||||
func (m *MockDownloader) GetLength(ctx context.Context, url string) (int64, error) {
|
||||
if m.getLengthFunc != nil {
|
||||
return m.getLengthFunc(ctx, url)
|
||||
}
|
||||
return 1024, nil
|
||||
}
|
||||
|
||||
type MockChecksumStorage struct {
|
||||
getFunc func(string) (*utils.ChecksumInfo, error)
|
||||
updateFunc func(string, *utils.ChecksumInfo) error
|
||||
}
|
||||
|
||||
func (m *MockChecksumStorage) Get(path string) (*utils.ChecksumInfo, error) {
|
||||
if m.getFunc != nil {
|
||||
return m.getFunc(path)
|
||||
}
|
||||
return &utils.ChecksumInfo{}, nil
|
||||
}
|
||||
|
||||
func (m *MockChecksumStorage) Update(path string, c *utils.ChecksumInfo) error {
|
||||
if m.updateFunc != nil {
|
||||
return m.updateFunc(path, c)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Test interfaces and their basic functionality
|
||||
|
||||
func (s *AptlySuite) TestPackagePoolInterface(c *C) {
|
||||
// Test PackagePool interface with mock implementation
|
||||
var pool PackagePool = &MockPackagePool{}
|
||||
|
||||
checksums := &utils.ChecksumInfo{}
|
||||
mockStorage := &MockChecksumStorage{}
|
||||
|
||||
// Test Verify
|
||||
path, exists, err := pool.Verify("test/path", "package.deb", checksums, mockStorage)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(exists, Equals, true)
|
||||
c.Check(path, Equals, "test/path")
|
||||
|
||||
// Test Import
|
||||
importedPath, err := pool.Import("/src/package.deb", "package.deb", checksums, false, mockStorage)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(importedPath, Equals, "imported/path/package.deb")
|
||||
|
||||
// Test LegacyPath
|
||||
legacyPath, err := pool.LegacyPath("package.deb", checksums)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(legacyPath, Equals, "legacy/package.deb")
|
||||
|
||||
// Test Size
|
||||
size, err := pool.Size("test/path")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(size, Equals, int64(1024))
|
||||
|
||||
// Test Open
|
||||
reader, err := pool.Open("test/path")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(reader, NotNil)
|
||||
reader.Close()
|
||||
|
||||
// Test FilepathList
|
||||
mockProgress := &MockProgress{}
|
||||
files, err := pool.FilepathList(mockProgress)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(len(files), Equals, 2)
|
||||
c.Check(files[0], Equals, "file1.deb")
|
||||
|
||||
// Test Remove
|
||||
removedSize, err := pool.Remove("test/path")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(removedSize, Equals, int64(1024))
|
||||
}
|
||||
|
||||
func (s *AptlySuite) TestPublishedStorageInterface(c *C) {
|
||||
// Test PublishedStorage interface with mock implementation
|
||||
var storage PublishedStorage = &MockPublishedStorage{}
|
||||
|
||||
// Test MkDir
|
||||
err := storage.MkDir("test/dir")
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Test PutFile
|
||||
err = storage.PutFile("dest/path", "source/file")
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Test RemoveDirs
|
||||
mockProgress := &MockProgress{}
|
||||
err = storage.RemoveDirs("test/dir", mockProgress)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Test Remove
|
||||
err = storage.Remove("test/file")
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Test LinkFromPool
|
||||
mockPool := &MockPackagePool{}
|
||||
checksums := utils.ChecksumInfo{}
|
||||
err = storage.LinkFromPool("prefix", "rel/path", "file.deb", mockPool, "pool/path", checksums, false)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Test Filelist
|
||||
files, err := storage.Filelist("prefix")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(len(files), Equals, 2)
|
||||
|
||||
// Test RenameFile
|
||||
err = storage.RenameFile("old", "new")
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Test SymLink
|
||||
err = storage.SymLink("src", "dst")
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Test HardLink
|
||||
err = storage.HardLink("src", "dst")
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Test FileExists
|
||||
exists, err := storage.FileExists("test/file")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(exists, Equals, true)
|
||||
|
||||
// Test ReadLink
|
||||
target, err := storage.ReadLink("link")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(target, Equals, "target")
|
||||
}
|
||||
|
||||
func (s *AptlySuite) TestProgressInterface(c *C) {
|
||||
// Test Progress interface with mock implementation
|
||||
var progress Progress = &MockProgress{}
|
||||
|
||||
// Test Start/Shutdown
|
||||
progress.Start()
|
||||
progress.Shutdown()
|
||||
|
||||
// Test Write
|
||||
n, err := progress.Write([]byte("test"))
|
||||
c.Check(err, IsNil)
|
||||
c.Check(n, Equals, 4)
|
||||
|
||||
// Test progress bar functions
|
||||
progress.InitBar(100, false, BarGeneralBuildPackageList)
|
||||
progress.AddBar(10)
|
||||
progress.SetBar(50)
|
||||
progress.ShutdownBar()
|
||||
|
||||
// Test Printf functions
|
||||
progress.Printf("test %s", "message")
|
||||
progress.ColoredPrintf("colored %s", "message")
|
||||
progress.PrintfStdErr("error %s", "message")
|
||||
|
||||
// Test Flush
|
||||
progress.Flush()
|
||||
}
|
||||
|
||||
func (s *AptlySuite) TestDownloaderInterface(c *C) {
|
||||
// Test Downloader interface with mock implementation
|
||||
var downloader Downloader = &MockDownloader{}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test Download
|
||||
err := downloader.Download(ctx, "http://example.com/file", "/tmp/dest")
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Test DownloadWithChecksum
|
||||
checksums := &utils.ChecksumInfo{}
|
||||
err = downloader.DownloadWithChecksum(ctx, "http://example.com/file", "/tmp/dest", checksums, false)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Test GetProgress
|
||||
progress := downloader.GetProgress()
|
||||
c.Check(progress, NotNil)
|
||||
|
||||
// Test GetLength
|
||||
length, err := downloader.GetLength(ctx, "http://example.com/file")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(length, Equals, int64(1024))
|
||||
}
|
||||
|
||||
func (s *AptlySuite) TestChecksumStorageInterface(c *C) {
|
||||
// Test ChecksumStorage interface with mock implementation
|
||||
var storage ChecksumStorage = &MockChecksumStorage{}
|
||||
|
||||
// Test Get
|
||||
checksums, err := storage.Get("test/path")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(checksums, NotNil)
|
||||
|
||||
// Test Update
|
||||
newChecksums := &utils.ChecksumInfo{}
|
||||
err = storage.Update("test/path", newChecksums)
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *AptlySuite) TestConsoleResultReporter(c *C) {
|
||||
// Test ConsoleResultReporter implementation
|
||||
mockProgress := &MockProgress{}
|
||||
reporter := &ConsoleResultReporter{Progress: mockProgress}
|
||||
|
||||
// Test interface compliance
|
||||
var _ ResultReporter = reporter
|
||||
|
||||
// Test Warning
|
||||
reporter.Warning("test warning %s", "message")
|
||||
output := mockProgress.buffer.String()
|
||||
c.Check(strings.Contains(output, "test warning message"), Equals, true)
|
||||
c.Check(strings.Contains(output, "[!]"), Equals, true)
|
||||
|
||||
// Reset buffer
|
||||
mockProgress.buffer.Reset()
|
||||
|
||||
// Test Removed
|
||||
reporter.Removed("removed %s", "item")
|
||||
output = mockProgress.buffer.String()
|
||||
c.Check(strings.Contains(output, "removed item"), Equals, true)
|
||||
c.Check(strings.Contains(output, "[-]"), Equals, true)
|
||||
|
||||
// Reset buffer
|
||||
mockProgress.buffer.Reset()
|
||||
|
||||
// Test Added
|
||||
reporter.Added("added %s", "item")
|
||||
output = mockProgress.buffer.String()
|
||||
c.Check(strings.Contains(output, "added item"), Equals, true)
|
||||
c.Check(strings.Contains(output, "[+]"), Equals, true)
|
||||
}
|
||||
|
||||
func (s *AptlySuite) TestRecordingResultReporter(c *C) {
|
||||
// Test RecordingResultReporter implementation
|
||||
reporter := &RecordingResultReporter{
|
||||
Warnings: []string{},
|
||||
AddedLines: []string{},
|
||||
RemovedLines: []string{},
|
||||
}
|
||||
|
||||
// Test interface compliance
|
||||
var _ ResultReporter = reporter
|
||||
|
||||
// Test Warning
|
||||
reporter.Warning("test warning %s", "message")
|
||||
c.Check(len(reporter.Warnings), Equals, 1)
|
||||
c.Check(reporter.Warnings[0], Equals, "test warning message")
|
||||
|
||||
// Test Removed
|
||||
reporter.Removed("removed %s", "item")
|
||||
c.Check(len(reporter.RemovedLines), Equals, 1)
|
||||
c.Check(reporter.RemovedLines[0], Equals, "removed item")
|
||||
|
||||
// Test Added
|
||||
reporter.Added("added %s", "item")
|
||||
c.Check(len(reporter.AddedLines), Equals, 1)
|
||||
c.Check(reporter.AddedLines[0], Equals, "added item")
|
||||
|
||||
// Test multiple entries
|
||||
reporter.Warning("second warning")
|
||||
reporter.Added("second addition")
|
||||
c.Check(len(reporter.Warnings), Equals, 2)
|
||||
c.Check(len(reporter.AddedLines), Equals, 2)
|
||||
c.Check(reporter.Warnings[1], Equals, "second warning")
|
||||
c.Check(reporter.AddedLines[1], Equals, "second addition")
|
||||
}
|
||||
|
||||
func (s *AptlySuite) TestReadSeekerCloserInterface(c *C) {
|
||||
// Test ReadSeekerCloser interface with mock implementation
|
||||
var rsc ReadSeekerCloser = &MockReadSeekerCloser{
|
||||
content: []byte("Hello, World!"),
|
||||
}
|
||||
|
||||
// Test Read
|
||||
buf := make([]byte, 5)
|
||||
n, err := rsc.Read(buf)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(n, Equals, 5)
|
||||
c.Check(string(buf), Equals, "Hello")
|
||||
|
||||
// Test Seek
|
||||
pos, err := rsc.Seek(0, io.SeekStart)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(pos, Equals, int64(0))
|
||||
|
||||
// Test Read again from beginning
|
||||
n, err = rsc.Read(buf)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(string(buf), Equals, "Hello")
|
||||
|
||||
// Test Seek to end
|
||||
pos, err = rsc.Seek(-6, io.SeekEnd)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(pos, Equals, int64(7))
|
||||
|
||||
// Test Read from near end
|
||||
buf = make([]byte, 10)
|
||||
n, err = rsc.Read(buf)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(string(buf[:n]), Equals, "World!")
|
||||
|
||||
// Test Close
|
||||
err = rsc.Close()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Test Read after close (should error)
|
||||
_, err = rsc.Read(buf)
|
||||
c.Check(err, NotNil)
|
||||
}
|
||||
|
||||
func (s *AptlySuite) TestBarTypeConstants(c *C) {
|
||||
// Test BarType constants are defined and different
|
||||
barTypes := []BarType{
|
||||
BarGeneralBuildPackageList,
|
||||
BarGeneralVerifyDependencies,
|
||||
BarGeneralBuildFileList,
|
||||
BarCleanupBuildList,
|
||||
BarCleanupDeleteUnreferencedFiles,
|
||||
BarMirrorUpdateDownloadIndexes,
|
||||
BarMirrorUpdateDownloadPackages,
|
||||
BarMirrorUpdateBuildPackageList,
|
||||
BarMirrorUpdateImportFiles,
|
||||
BarMirrorUpdateFinalizeDownload,
|
||||
BarPublishGeneratePackageFiles,
|
||||
BarPublishFinalizeIndexes,
|
||||
}
|
||||
|
||||
// Check that all constants are different
|
||||
seen := make(map[BarType]bool)
|
||||
for _, barType := range barTypes {
|
||||
c.Check(seen[barType], Equals, false, Commentf("Duplicate BarType: %v", barType))
|
||||
seen[barType] = true
|
||||
}
|
||||
|
||||
// Check that they are sequential integers starting from 0
|
||||
for i, barType := range barTypes {
|
||||
c.Check(int(barType), Equals, i, Commentf("BarType not sequential: %v", barType))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AptlySuite) TestErrorHandling(c *C) {
|
||||
// Test error handling in mock implementations
|
||||
|
||||
// Test PackagePool with errors
|
||||
pool := &MockPackagePool{
|
||||
verifyFunc: func(string, string, *utils.ChecksumInfo, ChecksumStorage) (string, bool, error) {
|
||||
return "", false, errors.New("verify error")
|
||||
},
|
||||
importFunc: func(string, string, *utils.ChecksumInfo, bool, ChecksumStorage) (string, error) {
|
||||
return "", errors.New("import error")
|
||||
},
|
||||
}
|
||||
|
||||
_, _, err := pool.Verify("", "", nil, nil)
|
||||
c.Check(err, NotNil)
|
||||
c.Check(err.Error(), Equals, "verify error")
|
||||
|
||||
_, err = pool.Import("", "", nil, false, nil)
|
||||
c.Check(err, NotNil)
|
||||
c.Check(err.Error(), Equals, "import error")
|
||||
|
||||
// Test PublishedStorage with errors
|
||||
storage := &MockPublishedStorage{
|
||||
mkDirFunc: func(string) error {
|
||||
return errors.New("mkdir error")
|
||||
},
|
||||
fileExistsFunc: func(string) (bool, error) {
|
||||
return false, errors.New("file exists error")
|
||||
},
|
||||
}
|
||||
|
||||
err = storage.MkDir("test")
|
||||
c.Check(err, NotNil)
|
||||
c.Check(err.Error(), Equals, "mkdir error")
|
||||
|
||||
_, err = storage.FileExists("test")
|
||||
c.Check(err, NotNil)
|
||||
c.Check(err.Error(), Equals, "file exists error")
|
||||
}
|
||||
|
||||
func (s *AptlySuite) TestInterfaceCompatibility(c *C) {
|
||||
// Test that our mocks properly implement the interfaces
|
||||
|
||||
// PackagePool interface
|
||||
var _ PackagePool = &MockPackagePool{}
|
||||
|
||||
// PublishedStorage interface
|
||||
var _ PublishedStorage = &MockPublishedStorage{}
|
||||
|
||||
// Progress interface
|
||||
var _ Progress = &MockProgress{}
|
||||
|
||||
// Downloader interface
|
||||
var _ Downloader = &MockDownloader{}
|
||||
|
||||
// ChecksumStorage interface
|
||||
var _ ChecksumStorage = &MockChecksumStorage{}
|
||||
|
||||
// ReadSeekerCloser interface
|
||||
var _ ReadSeekerCloser = &MockReadSeekerCloser{}
|
||||
|
||||
// ResultReporter interface
|
||||
var _ ResultReporter = &ConsoleResultReporter{}
|
||||
var _ ResultReporter = &RecordingResultReporter{}
|
||||
|
||||
// Test that the interface checks pass
|
||||
c.Check(true, Equals, true)
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package aptly
|
||||
|
||||
// AptlyConf holds the default aptly.conf (filled in at link time)
|
||||
// Default aptly.conf (filled in at link time)
|
||||
var AptlyConf []byte
|
||||
|
||||
@@ -85,8 +85,6 @@ type PublishedStorage interface {
|
||||
FileExists(path string) (bool, error)
|
||||
// ReadLink returns the symbolic link pointed to by path
|
||||
ReadLink(path string) (string, error)
|
||||
// Flush waits for any pending operations to complete (used by concurrent upload implementations)
|
||||
Flush() error
|
||||
}
|
||||
|
||||
// FileSystemPublishedStorage is published storage on filesystem
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package aptly
|
||||
|
||||
import (
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type InterfacesSuite struct{}
|
||||
|
||||
var _ = Suite(&InterfacesSuite{})
|
||||
|
||||
func (s *InterfacesSuite) TestBarTypeValues(c *C) {
|
||||
// Test that BarType enum values are as expected
|
||||
c.Check(int(BarGeneralBuildPackageList), Equals, 0)
|
||||
c.Check(int(BarGeneralVerifyDependencies), Equals, 1)
|
||||
c.Check(int(BarGeneralBuildFileList), Equals, 2)
|
||||
c.Check(int(BarCleanupBuildList), Equals, 3)
|
||||
c.Check(int(BarCleanupDeleteUnreferencedFiles), Equals, 4)
|
||||
c.Check(int(BarMirrorUpdateDownloadIndexes), Equals, 5)
|
||||
c.Check(int(BarMirrorUpdateDownloadPackages), Equals, 6)
|
||||
c.Check(int(BarMirrorUpdateBuildPackageList), Equals, 7)
|
||||
c.Check(int(BarMirrorUpdateImportFiles), Equals, 8)
|
||||
c.Check(int(BarMirrorUpdateFinalizeDownload), Equals, 9)
|
||||
c.Check(int(BarPublishGeneratePackageFiles), Equals, 10)
|
||||
c.Check(int(BarPublishFinalizeIndexes), Equals, 11)
|
||||
}
|
||||
@@ -29,7 +29,7 @@ func NewPackagePool(accountName, accountKey, container, prefix, endpoint string)
|
||||
return &PackagePool{az: azctx}, nil
|
||||
}
|
||||
|
||||
// String returns the storage as string
|
||||
// String
|
||||
func (pool *PackagePool) String() string {
|
||||
return pool.az.String()
|
||||
}
|
||||
@@ -104,7 +104,7 @@ func (pool *PackagePool) Open(path string) (aptly.ReadSeekerCloser, error) {
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error creating tempfile for %s", path)
|
||||
}
|
||||
defer func() { _ = os.Remove(temp.Name()) }()
|
||||
defer os.Remove(temp.Name())
|
||||
|
||||
_, err = pool.az.client.DownloadFile(context.TODO(), pool.az.container, path, temp, nil)
|
||||
if err != nil {
|
||||
@@ -156,7 +156,7 @@ func (pool *PackagePool) Import(srcPath, basename string, checksums *utils.Check
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func() { _ = source.Close() }()
|
||||
defer source.Close()
|
||||
|
||||
err = pool.az.putFile(path, source, checksums.MD5)
|
||||
if err != nil {
|
||||
|
||||
+11
-11
@@ -2,12 +2,12 @@ package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/files"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
@@ -50,10 +50,10 @@ func (s *PackagePoolSuite) SetUpTest(c *C) {
|
||||
|
||||
s.pool, err = NewPackagePool(s.accountName, s.accountKey, container, "", s.endpoint)
|
||||
c.Assert(err, IsNil)
|
||||
publicAccessType := azblob.PublicAccessTypeContainer
|
||||
_, err = s.pool.az.client.CreateContainer(context.TODO(), s.pool.az.container, &azblob.CreateContainerOptions{
|
||||
Access: &publicAccessType,
|
||||
})
|
||||
publicAccessType := azblob.PublicAccessTypeContainer
|
||||
_, err = s.pool.az.client.CreateContainer(context.TODO(), s.pool.az.container, &azblob.CreateContainerOptions{
|
||||
Access: &publicAccessType,
|
||||
})
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
s.prefixedPool, err = NewPackagePool(s.accountName, s.accountKey, container, prefix, s.endpoint)
|
||||
@@ -69,8 +69,8 @@ func (s *PackagePoolSuite) TestFilepathList(c *C) {
|
||||
c.Check(err, IsNil)
|
||||
c.Check(list, DeepEquals, []string{})
|
||||
|
||||
_, _ = s.pool.Import(s.debFile, "a.deb", &utils.ChecksumInfo{}, false, s.cs)
|
||||
_, _ = s.pool.Import(s.debFile, "b.deb", &utils.ChecksumInfo{}, false, s.cs)
|
||||
s.pool.Import(s.debFile, "a.deb", &utils.ChecksumInfo{}, false, s.cs)
|
||||
s.pool.Import(s.debFile, "b.deb", &utils.ChecksumInfo{}, false, s.cs)
|
||||
|
||||
list, err = s.pool.FilepathList(nil)
|
||||
c.Check(err, IsNil)
|
||||
@@ -81,8 +81,8 @@ func (s *PackagePoolSuite) TestFilepathList(c *C) {
|
||||
}
|
||||
|
||||
func (s *PackagePoolSuite) TestRemove(c *C) {
|
||||
_, _ = s.pool.Import(s.debFile, "a.deb", &utils.ChecksumInfo{}, false, s.cs)
|
||||
_, _ = s.pool.Import(s.debFile, "b.deb", &utils.ChecksumInfo{}, false, s.cs)
|
||||
s.pool.Import(s.debFile, "a.deb", &utils.ChecksumInfo{}, false, s.cs)
|
||||
s.pool.Import(s.debFile, "b.deb", &utils.ChecksumInfo{}, false, s.cs)
|
||||
|
||||
size, err := s.pool.Remove("c7/6b/4bd12fd92e4dfe1b55b18a67a669_a.deb")
|
||||
c.Check(err, IsNil)
|
||||
@@ -247,7 +247,7 @@ func (s *PackagePoolSuite) TestOpen(c *C) {
|
||||
|
||||
f, err := s.pool.Open(path)
|
||||
c.Assert(err, IsNil)
|
||||
contents, err := io.ReadAll(f)
|
||||
contents, err := ioutil.ReadAll(f)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(len(contents), Equals, 2738)
|
||||
c.Check(f.Close(), IsNil)
|
||||
|
||||
+5
-12
@@ -18,7 +18,7 @@ import (
|
||||
|
||||
// PublishedStorage abstract file system with published files (actually hosted on Azure)
|
||||
type PublishedStorage struct {
|
||||
// FIXME: unused ???? prefix string
|
||||
prefix string
|
||||
az *azContext
|
||||
pathCache map[string]map[string]string
|
||||
}
|
||||
@@ -38,7 +38,7 @@ func NewPublishedStorage(accountName, accountKey, container, prefix, endpoint st
|
||||
return &PublishedStorage{az: azctx}, nil
|
||||
}
|
||||
|
||||
// String returns the storage as string
|
||||
// String
|
||||
func (storage *PublishedStorage) String() string {
|
||||
return storage.az.String()
|
||||
}
|
||||
@@ -65,7 +65,7 @@ func (storage *PublishedStorage) PutFile(path string, sourceFilename string) err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = source.Close() }()
|
||||
defer source.Close()
|
||||
|
||||
err = storage.az.putFile(path, source, sourceMD5)
|
||||
if err != nil {
|
||||
@@ -158,7 +158,7 @@ func (storage *PublishedStorage) LinkFromPool(publishedPrefix, publishedRelPath,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = source.Close() }()
|
||||
defer source.Close()
|
||||
|
||||
err = storage.az.putFile(relFilePath, source, sourceMD5)
|
||||
if err == nil {
|
||||
@@ -193,9 +193,7 @@ func (storage *PublishedStorage) internalCopyOrMoveBlob(src, dst string, metadat
|
||||
if err != nil {
|
||||
return fmt.Errorf("error acquiring lease on source blob %s", src)
|
||||
}
|
||||
defer func() {
|
||||
_, _ = blobLeaseClient.BreakLease(context.Background(), &lease.BlobBreakOptions{BreakPeriod: to.Ptr(int32(60))})
|
||||
}()
|
||||
defer blobLeaseClient.BreakLease(context.Background(), &lease.BlobBreakOptions{BreakPeriod: to.Ptr(int32(60))})
|
||||
|
||||
dstBlobClient := containerClient.NewBlobClient(dst)
|
||||
copyResp, err := dstBlobClient.StartCopyFromURL(context.Background(), srcBlobClient.URL(), &blob.StartCopyFromURLOptions{
|
||||
@@ -287,8 +285,3 @@ func (storage *PublishedStorage) ReadLink(path string) (string, error) {
|
||||
}
|
||||
return "", fmt.Errorf("error reading link %s: %v", path, err)
|
||||
}
|
||||
|
||||
// Flush is a no-op for Azure storage
|
||||
func (storage *PublishedStorage) Flush() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
+32
-32
@@ -1,17 +1,17 @@
|
||||
package azure
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"bytes"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob"
|
||||
"github.com/aptly-dev/aptly/files"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
. "gopkg.in/check.v1"
|
||||
@@ -36,7 +36,7 @@ func randString(n int) string {
|
||||
}
|
||||
const alphanum = "0123456789abcdefghijklmnopqrstuvwxyz"
|
||||
var bytes = make([]byte, n)
|
||||
_, _ = rand.Read(bytes)
|
||||
rand.Read(bytes)
|
||||
for i, b := range bytes {
|
||||
bytes[i] = alphanum[b%byte(len(alphanum))]
|
||||
}
|
||||
@@ -69,10 +69,10 @@ func (s *PublishedStorageSuite) SetUpTest(c *C) {
|
||||
|
||||
s.storage, err = NewPublishedStorage(s.accountName, s.accountKey, container, "", s.endpoint)
|
||||
c.Assert(err, IsNil)
|
||||
publicAccessType := azblob.PublicAccessTypeContainer
|
||||
_, err = s.storage.az.client.CreateContainer(context.Background(), s.storage.az.container, &azblob.CreateContainerOptions{
|
||||
Access: &publicAccessType,
|
||||
})
|
||||
publicAccessType := azblob.PublicAccessTypeContainer
|
||||
_, err = s.storage.az.client.CreateContainer(context.Background(), s.storage.az.container, &azblob.CreateContainerOptions{
|
||||
Access: &publicAccessType,
|
||||
})
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
s.prefixedStorage, err = NewPublishedStorage(s.accountName, s.accountKey, container, prefix, s.endpoint)
|
||||
@@ -80,39 +80,39 @@ func (s *PublishedStorageSuite) SetUpTest(c *C) {
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TearDownTest(c *C) {
|
||||
_, err := s.storage.az.client.DeleteContainer(context.Background(), s.storage.az.container, nil)
|
||||
_, err := s.storage.az.client.DeleteContainer(context.Background(), s.storage.az.container, nil)
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) GetFile(c *C, path string) []byte {
|
||||
resp, err := s.storage.az.client.DownloadStream(context.Background(), s.storage.az.container, path, nil)
|
||||
resp, err := s.storage.az.client.DownloadStream(context.Background(), s.storage.az.container, path, nil)
|
||||
c.Assert(err, IsNil)
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
c.Assert(err, IsNil)
|
||||
return data
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) AssertNoFile(c *C, path string) {
|
||||
serviceClient := s.storage.az.client.ServiceClient()
|
||||
containerClient := serviceClient.NewContainerClient(s.storage.az.container)
|
||||
blobClient := containerClient.NewBlobClient(path)
|
||||
_, err := blobClient.GetProperties(context.Background(), nil)
|
||||
serviceClient := s.storage.az.client.ServiceClient()
|
||||
containerClient := serviceClient.NewContainerClient(s.storage.az.container)
|
||||
blobClient := containerClient.NewBlobClient(path)
|
||||
_, err := blobClient.GetProperties(context.Background(), nil)
|
||||
c.Assert(err, NotNil)
|
||||
|
||||
storageError, ok := err.(*azcore.ResponseError)
|
||||
storageError, ok := err.(*azcore.ResponseError)
|
||||
c.Assert(ok, Equals, true)
|
||||
c.Assert(storageError.StatusCode, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) PutFile(c *C, path string, data []byte) {
|
||||
hash := md5.Sum(data)
|
||||
uploadOptions := &azblob.UploadStreamOptions{
|
||||
HTTPHeaders: &blob.HTTPHeaders{
|
||||
BlobContentMD5: hash[:],
|
||||
},
|
||||
}
|
||||
reader := bytes.NewReader(data)
|
||||
_, err := s.storage.az.client.UploadStream(context.Background(), s.storage.az.container, path, reader, uploadOptions)
|
||||
uploadOptions := &azblob.UploadStreamOptions{
|
||||
HTTPHeaders: &blob.HTTPHeaders{
|
||||
BlobContentMD5: hash[:],
|
||||
},
|
||||
}
|
||||
reader := bytes.NewReader(data)
|
||||
_, err := s.storage.az.client.UploadStream(context.Background(), s.storage.az.container, path, reader, uploadOptions)
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ func (s *PublishedStorageSuite) TestPutFile(c *C) {
|
||||
filename := "a/b.txt"
|
||||
|
||||
dir := c.MkDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "a"), content, 0644)
|
||||
err := ioutil.WriteFile(filepath.Join(dir, "a"), content, 0644)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = s.storage.PutFile(filename, filepath.Join(dir, "a"))
|
||||
@@ -140,7 +140,7 @@ func (s *PublishedStorageSuite) TestPutFilePlus(c *C) {
|
||||
filename := "a/b+c.txt"
|
||||
|
||||
dir := c.MkDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "a"), content, 0644)
|
||||
err := ioutil.WriteFile(filepath.Join(dir, "a"), content, 0644)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = s.storage.PutFile(filename, filepath.Join(dir, "a"))
|
||||
@@ -258,7 +258,7 @@ func (s *PublishedStorageSuite) TestRemoveDirsPlus(c *C) {
|
||||
|
||||
func (s *PublishedStorageSuite) TestRenameFile(c *C) {
|
||||
dir := c.MkDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "a"), []byte("Welcome to Azure!"), 0644)
|
||||
err := ioutil.WriteFile(filepath.Join(dir, "a"), []byte("Welcome to Azure!"), 0644)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = s.storage.PutFile("source.txt", filepath.Join(dir, "a"))
|
||||
@@ -280,18 +280,18 @@ func (s *PublishedStorageSuite) TestLinkFromPool(c *C) {
|
||||
cs := files.NewMockChecksumStorage()
|
||||
|
||||
tmpFile1 := filepath.Join(c.MkDir(), "mars-invaders_1.03.deb")
|
||||
err := os.WriteFile(tmpFile1, []byte("Contents"), 0644)
|
||||
err := ioutil.WriteFile(tmpFile1, []byte("Contents"), 0644)
|
||||
c.Assert(err, IsNil)
|
||||
cksum1 := utils.ChecksumInfo{MD5: "c1df1da7a1ce305a3b60af9d5733ac1d"}
|
||||
|
||||
tmpFile2 := filepath.Join(c.MkDir(), "mars-invaders_1.03.deb")
|
||||
err = os.WriteFile(tmpFile2, []byte("Spam"), 0644)
|
||||
err = ioutil.WriteFile(tmpFile2, []byte("Spam"), 0644)
|
||||
c.Assert(err, IsNil)
|
||||
cksum2 := utils.ChecksumInfo{MD5: "e9dfd31cc505d51fc26975250750deab"}
|
||||
|
||||
tmpFile3 := filepath.Join(c.MkDir(), "netboot/boot.img.gz")
|
||||
_ = os.MkdirAll(filepath.Dir(tmpFile3), 0777)
|
||||
err = os.WriteFile(tmpFile3, []byte("Contents"), 0644)
|
||||
os.MkdirAll(filepath.Dir(tmpFile3), 0777)
|
||||
err = ioutil.WriteFile(tmpFile3, []byte("Contents"), 0644)
|
||||
c.Assert(err, IsNil)
|
||||
cksum3 := utils.ChecksumInfo{MD5: "c1df1da7a1ce305a3b60af9d5733ac1d"}
|
||||
|
||||
|
||||
+4
-4
@@ -46,7 +46,7 @@ func aptlyAPIServe(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
if err == nil && len(listeners) == 1 {
|
||||
listener := listeners[0]
|
||||
defer func() { _ = listener.Close() }()
|
||||
defer listener.Close()
|
||||
fmt.Printf("\nTaking over web server at: %s (press Ctrl+C to quit)...\n", listener.Addr().String())
|
||||
err = http.Serve(listener, api.Router(context))
|
||||
if err != nil {
|
||||
@@ -67,7 +67,7 @@ func aptlyAPIServe(cmd *commander.Command, args []string) error {
|
||||
if _, ok := <-sigchan; ok {
|
||||
fmt.Printf("\nShutdown signal received, waiting for background tasks...\n")
|
||||
context.TaskList().Wait()
|
||||
_ = server.Shutdown(stdcontext.Background())
|
||||
server.Shutdown(stdcontext.Background())
|
||||
}
|
||||
})()
|
||||
defer close(sigchan)
|
||||
@@ -75,14 +75,14 @@ func aptlyAPIServe(cmd *commander.Command, args []string) error {
|
||||
listenURL, err := url.Parse(listen)
|
||||
if err == nil && listenURL.Scheme == "unix" {
|
||||
file := listenURL.Path
|
||||
_ = os.Remove(file)
|
||||
os.Remove(file)
|
||||
|
||||
var listener net.Listener
|
||||
listener, err = net.Listen("unix", file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to listen on: %s\n%s", file, err)
|
||||
}
|
||||
defer func() { _ = listener.Close() }()
|
||||
defer listener.Close()
|
||||
|
||||
err = server.Serve(listener)
|
||||
} else {
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@ package environment to new version.`,
|
||||
Flag: *flag.NewFlagSet("aptly", flag.ExitOnError),
|
||||
Subcommands: []*commander.Command{
|
||||
makeCmdConfig(),
|
||||
makeCmdDB(),
|
||||
makeCmdDb(),
|
||||
makeCmdGraph(),
|
||||
makeCmdMirror(),
|
||||
makeCmdRepo(),
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/smira/flag"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type CmdSuite struct {
|
||||
mockProgress *MockCmdProgress
|
||||
collectionFactory *deb.CollectionFactory
|
||||
mockContext *MockCmdContext
|
||||
}
|
||||
|
||||
var _ = Suite(&CmdSuite{})
|
||||
|
||||
func (s *CmdSuite) SetUpTest(c *C) {
|
||||
s.mockProgress = &MockCmdProgress{}
|
||||
|
||||
// Set up mock collections - use real collection factory
|
||||
s.collectionFactory = deb.NewCollectionFactory(nil)
|
||||
|
||||
// Set up mock context
|
||||
s.mockContext = &MockCmdContext{
|
||||
progress: s.mockProgress,
|
||||
collectionFactory: s.collectionFactory,
|
||||
}
|
||||
|
||||
// Skip setting mock context globally for type compatibility
|
||||
// context = s.mockContext
|
||||
}
|
||||
|
||||
func (s *CmdSuite) TestListPackagesRefListBasic(c *C) {
|
||||
// Test basic functionality of ListPackagesRefList
|
||||
// Need to initialize context for this test
|
||||
if context == nil {
|
||||
flags := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
err := InitContext(flags)
|
||||
c.Assert(err, IsNil)
|
||||
defer ShutdownContext()
|
||||
}
|
||||
|
||||
reflist := &deb.PackageRefList{}
|
||||
|
||||
err := ListPackagesRefList(reflist, s.collectionFactory)
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *CmdSuite) TestPrintPackageListBasic(c *C) {
|
||||
// Test basic PrintPackageList functionality
|
||||
packageList := deb.NewPackageList()
|
||||
|
||||
err := PrintPackageList(packageList, "", " ")
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
// Mock implementations for testing
|
||||
|
||||
type MockCmdProgress struct {
|
||||
messages []string
|
||||
}
|
||||
|
||||
func (m *MockCmdProgress) Printf(msg string, a ...interface{}) {}
|
||||
func (m *MockCmdProgress) ColoredPrintf(msg string, a ...interface{}) {}
|
||||
func (m *MockCmdProgress) PrintfStdErr(msg string, a ...interface{}) {}
|
||||
func (m *MockCmdProgress) Flush() {}
|
||||
func (m *MockCmdProgress) Start() {}
|
||||
func (m *MockCmdProgress) Shutdown() {}
|
||||
func (m *MockCmdProgress) InitBar(count int64, isBytes bool, barType aptly.BarType) {}
|
||||
func (m *MockCmdProgress) ShutdownBar() {}
|
||||
func (m *MockCmdProgress) AddBar(count int) {}
|
||||
func (m *MockCmdProgress) SetBar(count int) {}
|
||||
func (m *MockCmdProgress) PrintfBar(msg string, a ...interface{}) {}
|
||||
func (m *MockCmdProgress) Write(p []byte) (n int, err error) { return len(p), nil }
|
||||
|
||||
type MockCmdContext struct {
|
||||
progress *MockCmdProgress
|
||||
collectionFactory *deb.CollectionFactory
|
||||
}
|
||||
|
||||
func (m *MockCmdContext) Flags() *flag.FlagSet { return &flag.FlagSet{} }
|
||||
func (m *MockCmdContext) Progress() aptly.Progress { return m.progress }
|
||||
func (m *MockCmdContext) NewCollectionFactory() *deb.CollectionFactory { return m.collectionFactory }
|
||||
func (m *MockCmdContext) Config() *utils.ConfigStructure { return &utils.ConfigStructure{} }
|
||||
|
||||
// Note: Complex integration tests have been simplified for compilation compatibility.
|
||||
+1
-1
@@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/smira/commander"
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func aptlyConfigShow(_ *commander.Command, _ []string) error {
|
||||
|
||||
+2
-6
@@ -9,16 +9,12 @@ var context *ctx.AptlyContext
|
||||
|
||||
// ShutdownContext shuts context down
|
||||
func ShutdownContext() {
|
||||
if context != nil {
|
||||
context.Shutdown()
|
||||
}
|
||||
context.Shutdown()
|
||||
}
|
||||
|
||||
// CleanupContext does partial shutdown of context
|
||||
func CleanupContext() {
|
||||
if context != nil {
|
||||
context.Cleanup()
|
||||
}
|
||||
context.Cleanup()
|
||||
}
|
||||
|
||||
// InitContext initializes context with default settings
|
||||
|
||||
@@ -1,240 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
ctx "github.com/aptly-dev/aptly/context"
|
||||
"github.com/smira/flag"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
// Hook up gocheck into the "go test" runner.
|
||||
func Test(t *testing.T) { TestingT(t) }
|
||||
|
||||
type ContextSuite struct {
|
||||
originalContext *ctx.AptlyContext
|
||||
}
|
||||
|
||||
var _ = Suite(&ContextSuite{})
|
||||
|
||||
func (s *ContextSuite) SetUpTest(c *C) {
|
||||
// Save original context state
|
||||
s.originalContext = context
|
||||
context = nil // Reset context for each test
|
||||
}
|
||||
|
||||
func (s *ContextSuite) TearDownTest(c *C) {
|
||||
// Clean up and restore original context
|
||||
if context != nil {
|
||||
context.Shutdown()
|
||||
context = nil
|
||||
}
|
||||
context = s.originalContext
|
||||
}
|
||||
|
||||
func (s *ContextSuite) TestInitContextSuccess(c *C) {
|
||||
// Test successful context initialization
|
||||
flags := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
|
||||
err := InitContext(flags)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(context, NotNil)
|
||||
c.Check(GetContext(), Equals, context)
|
||||
}
|
||||
|
||||
func (s *ContextSuite) TestInitContextPanic(c *C) {
|
||||
// Test that initializing context twice causes panic
|
||||
flags := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
|
||||
// First initialization should succeed
|
||||
err := InitContext(flags)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(context, NotNil)
|
||||
|
||||
// Second initialization should panic
|
||||
c.Check(func() { InitContext(flags) }, Panics, "context already initialized")
|
||||
}
|
||||
|
||||
func (s *ContextSuite) TestInitContextError(c *C) {
|
||||
// Test context initialization with invalid flags
|
||||
// This tests the error path where ctx.NewContext might fail
|
||||
flags := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
|
||||
// Add some invalid flag configuration that might cause NewContext to fail
|
||||
// Note: This depends on the ctx.NewContext implementation details
|
||||
flags.String("invalid-config", "/nonexistent/path/to/config", "invalid config")
|
||||
flags.Set("invalid-config", "/nonexistent/path/to/config")
|
||||
|
||||
err := InitContext(flags)
|
||||
// The error handling depends on the ctx.NewContext implementation
|
||||
// If it doesn't fail with invalid paths, the test still validates the error path exists
|
||||
if err != nil {
|
||||
c.Check(context, IsNil)
|
||||
} else {
|
||||
c.Check(context, NotNil)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ContextSuite) TestGetContextBeforeInit(c *C) {
|
||||
// Test GetContext when context is nil
|
||||
c.Check(context, IsNil)
|
||||
result := GetContext()
|
||||
c.Check(result, IsNil)
|
||||
}
|
||||
|
||||
func (s *ContextSuite) TestGetContextAfterInit(c *C) {
|
||||
// Test GetContext after successful initialization
|
||||
flags := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
|
||||
err := InitContext(flags)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
result := GetContext()
|
||||
c.Check(result, NotNil)
|
||||
c.Check(result, Equals, context)
|
||||
}
|
||||
|
||||
func (s *ContextSuite) TestShutdownContext(c *C) {
|
||||
// Test ShutdownContext function
|
||||
flags := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
|
||||
err := InitContext(flags)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(context, NotNil)
|
||||
|
||||
// ShutdownContext should not panic and should call context.Shutdown()
|
||||
ShutdownContext() // Should not panic
|
||||
}
|
||||
|
||||
func (s *ContextSuite) TestShutdownContextNil(c *C) {
|
||||
// Test ShutdownContext when context is nil (should handle gracefully)
|
||||
context = nil
|
||||
|
||||
// Should not panic when context is nil
|
||||
ShutdownContext() // Should handle nil gracefully
|
||||
}
|
||||
|
||||
func (s *ContextSuite) TestCleanupContext(c *C) {
|
||||
// Test CleanupContext function
|
||||
flags := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
|
||||
err := InitContext(flags)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(context, NotNil)
|
||||
|
||||
// CleanupContext should not panic and should call context.Cleanup()
|
||||
CleanupContext() // Should not panic
|
||||
}
|
||||
|
||||
func (s *ContextSuite) TestCleanupContextNil(c *C) {
|
||||
// Test CleanupContext when context is nil (should handle gracefully)
|
||||
context = nil
|
||||
|
||||
// Should not panic when context is nil
|
||||
CleanupContext() // Should handle nil gracefully
|
||||
}
|
||||
|
||||
func (s *ContextSuite) TestContextLifecycle(c *C) {
|
||||
// Test complete context lifecycle: init -> use -> cleanup -> shutdown
|
||||
flags := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
|
||||
// Initialize
|
||||
err := InitContext(flags)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(context, NotNil)
|
||||
|
||||
// Use
|
||||
ctx := GetContext()
|
||||
c.Check(ctx, NotNil)
|
||||
c.Check(ctx, Equals, context)
|
||||
|
||||
// Cleanup
|
||||
CleanupContext() // Should not panic
|
||||
|
||||
// Context should still exist after cleanup
|
||||
c.Check(context, NotNil)
|
||||
c.Check(GetContext(), NotNil)
|
||||
|
||||
// Shutdown
|
||||
ShutdownContext() // Should not panic
|
||||
}
|
||||
|
||||
func (s *ContextSuite) TestMultipleCleanups(c *C) {
|
||||
// Test calling CleanupContext multiple times
|
||||
flags := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
|
||||
err := InitContext(flags)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Multiple cleanups should not cause issues
|
||||
CleanupContext() // First cleanup
|
||||
CleanupContext() // Second cleanup
|
||||
CleanupContext() // Third cleanup
|
||||
}
|
||||
|
||||
func (s *ContextSuite) TestContextVariableIsolation(c *C) {
|
||||
// Test that the context variable is properly managed
|
||||
c.Check(context, IsNil)
|
||||
|
||||
flags := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
err := InitContext(flags)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Store reference
|
||||
originalContext := context
|
||||
c.Check(originalContext, NotNil)
|
||||
|
||||
// GetContext should return the same instance
|
||||
retrievedContext := GetContext()
|
||||
c.Check(retrievedContext, Equals, originalContext)
|
||||
|
||||
// Context variable should be the same
|
||||
c.Check(context, Equals, originalContext)
|
||||
}
|
||||
|
||||
func (s *ContextSuite) TestFlagSetVariations(c *C) {
|
||||
// Test InitContext with different FlagSet configurations
|
||||
testCases := []struct {
|
||||
name string
|
||||
setupFn func() *flag.FlagSet
|
||||
}{
|
||||
{
|
||||
name: "empty flagset",
|
||||
setupFn: func() *flag.FlagSet {
|
||||
return flag.NewFlagSet("empty", flag.ContinueOnError)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "flagset with common flags",
|
||||
setupFn: func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("common", flag.ContinueOnError)
|
||||
fs.String("config", "", "config file")
|
||||
fs.Bool("debug", false, "debug mode")
|
||||
return fs
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "flagset with aptly-specific flags",
|
||||
setupFn: func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("aptly", flag.ContinueOnError)
|
||||
fs.String("architectures", "", "architectures")
|
||||
fs.String("distribution", "", "distribution")
|
||||
return fs
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
// Reset context for each test case
|
||||
if context != nil {
|
||||
context.Shutdown()
|
||||
context = nil
|
||||
}
|
||||
|
||||
flags := tc.setupFn()
|
||||
err := InitContext(flags)
|
||||
c.Check(err, IsNil, Commentf("Failed for test case: %s", tc.name))
|
||||
c.Check(context, NotNil, Commentf("Context is nil for test case: %s", tc.name))
|
||||
c.Check(GetContext(), NotNil, Commentf("GetContext returned nil for test case: %s", tc.name))
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,13 @@ import (
|
||||
"github.com/smira/commander"
|
||||
)
|
||||
|
||||
func makeCmdDB() *commander.Command {
|
||||
func makeCmdDb() *commander.Command {
|
||||
return &commander.Command{
|
||||
UsageLine: "db",
|
||||
Short: "manage aptly's internal database and package pool",
|
||||
Subcommands: []*commander.Command{
|
||||
makeCmdDBCleanup(),
|
||||
makeCmdDBRecover(),
|
||||
makeCmdDbCleanup(),
|
||||
makeCmdDbRecover(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
+7
-7
@@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
// aptly db cleanup
|
||||
func aptlyDBCleanup(cmd *commander.Command, args []string) error {
|
||||
func aptlyDbCleanup(cmd *commander.Command, args []string) error {
|
||||
var err error
|
||||
|
||||
if len(args) != 0 {
|
||||
@@ -48,7 +48,7 @@ func aptlyDBCleanup(cmd *commander.Command, args []string) error {
|
||||
|
||||
if verbose {
|
||||
description := fmt.Sprintf("mirror %s", repo.Name)
|
||||
_ = repo.RefList().ForEach(func(key []byte) error {
|
||||
repo.RefList().ForEach(func(key []byte) error {
|
||||
packageRefSources[string(key)] = append(packageRefSources[string(key)], description)
|
||||
return nil
|
||||
})
|
||||
@@ -81,7 +81,7 @@ func aptlyDBCleanup(cmd *commander.Command, args []string) error {
|
||||
|
||||
if verbose {
|
||||
description := fmt.Sprintf("local repo %s", repo.Name)
|
||||
_ = repo.RefList().ForEach(func(key []byte) error {
|
||||
repo.RefList().ForEach(func(key []byte) error {
|
||||
packageRefSources[string(key)] = append(packageRefSources[string(key)], description)
|
||||
return nil
|
||||
})
|
||||
@@ -113,7 +113,7 @@ func aptlyDBCleanup(cmd *commander.Command, args []string) error {
|
||||
|
||||
if verbose {
|
||||
description := fmt.Sprintf("snapshot %s", snapshot.Name)
|
||||
_ = snapshot.RefList().ForEach(func(key []byte) error {
|
||||
snapshot.RefList().ForEach(func(key []byte) error {
|
||||
packageRefSources[string(key)] = append(packageRefSources[string(key)], description)
|
||||
return nil
|
||||
})
|
||||
@@ -146,7 +146,7 @@ func aptlyDBCleanup(cmd *commander.Command, args []string) error {
|
||||
if verbose {
|
||||
description := fmt.Sprintf("published repository %s:%s/%s component %s",
|
||||
published.Storage, published.Prefix, published.Distribution, component)
|
||||
_ = published.RefList(component).ForEach(func(key []byte) error {
|
||||
published.RefList(component).ForEach(func(key []byte) error {
|
||||
packageRefSources[string(key)] = append(packageRefSources[string(key)], description)
|
||||
return nil
|
||||
})
|
||||
@@ -291,9 +291,9 @@ func aptlyDBCleanup(cmd *commander.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func makeCmdDBCleanup() *commander.Command {
|
||||
func makeCmdDbCleanup() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlyDBCleanup,
|
||||
Run: aptlyDbCleanup,
|
||||
UsageLine: "cleanup",
|
||||
Short: "cleanup DB and package pool",
|
||||
Long: `
|
||||
|
||||
+4
-45
@@ -1,16 +1,13 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/smira/commander"
|
||||
|
||||
"github.com/aptly-dev/aptly/database/goleveldb"
|
||||
)
|
||||
|
||||
// aptly db recover
|
||||
func aptlyDBRecover(cmd *commander.Command, args []string) error {
|
||||
func aptlyDbRecover(cmd *commander.Command, args []string) error {
|
||||
var err error
|
||||
|
||||
if len(args) != 0 {
|
||||
@@ -19,19 +16,14 @@ func aptlyDBRecover(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
|
||||
context.Progress().Printf("Recovering database...\n")
|
||||
if err = goleveldb.RecoverDB(context.DBPath()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
context.Progress().Printf("Checking database integrity...\n")
|
||||
err = checkIntegrity()
|
||||
err = goleveldb.RecoverDB(context.DBPath())
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func makeCmdDBRecover() *commander.Command {
|
||||
func makeCmdDbRecover() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlyDBRecover,
|
||||
Run: aptlyDbRecover,
|
||||
UsageLine: "recover",
|
||||
Short: "recover DB after crash",
|
||||
Long: `
|
||||
@@ -46,36 +38,3 @@ Example:
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func checkIntegrity() error {
|
||||
return context.NewCollectionFactory().LocalRepoCollection().ForEach(checkRepo)
|
||||
}
|
||||
|
||||
func checkRepo(repo *deb.LocalRepo) error {
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
repos := collectionFactory.LocalRepoCollection()
|
||||
|
||||
err := repos.LoadComplete(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load complete repo %q: %s", repo.Name, err)
|
||||
}
|
||||
|
||||
dangling, err := deb.FindDanglingReferences(repo.RefList(), collectionFactory.PackageCollection())
|
||||
if err != nil {
|
||||
return fmt.Errorf("find dangling references: %w", err)
|
||||
}
|
||||
|
||||
if len(dangling.Refs) > 0 {
|
||||
for _, ref := range dangling.Refs {
|
||||
context.Progress().Printf("Removing dangling database reference %q\n", ref)
|
||||
}
|
||||
|
||||
repo.UpdateRefList(repo.RefList().Subtract(dangling))
|
||||
|
||||
if err = repos.Update(repo); err != nil {
|
||||
return fmt.Errorf("update repo: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+2
-2
@@ -38,8 +38,8 @@ func aptlyGraph(cmd *commander.Command, args []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = tempfile.Close()
|
||||
_ = os.Remove(tempfile.Name())
|
||||
tempfile.Close()
|
||||
os.Remove(tempfile.Name())
|
||||
|
||||
format := context.Flags().Lookup("format").Value.String()
|
||||
output := context.Flags().Lookup("output").Value.String()
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ func getVerifier(flags *flag.FlagSet) (pgp.Verifier, error) {
|
||||
verifier.AddKeyring(keyRing)
|
||||
}
|
||||
|
||||
err := verifier.InitKeyring(!ignoreSignatures) // be verbose only if verifying signatures is requested
|
||||
err := verifier.InitKeyring(ignoreSignatures == false) // be verbose only if verifying signatures is requested
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+4
-4
@@ -32,7 +32,7 @@ func aptlyMirrorListTxt(cmd *commander.Command, _ []string) error {
|
||||
|
||||
repos := make([]string, collectionFactory.RemoteRepoCollection().Len())
|
||||
i := 0
|
||||
_ = collectionFactory.RemoteRepoCollection().ForEach(func(repo *deb.RemoteRepo) error {
|
||||
collectionFactory.RemoteRepoCollection().ForEach(func(repo *deb.RemoteRepo) error {
|
||||
if raw {
|
||||
repos[i] = repo.Name
|
||||
} else {
|
||||
@@ -42,7 +42,7 @@ func aptlyMirrorListTxt(cmd *commander.Command, _ []string) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
_ = context.CloseDatabase()
|
||||
context.CloseDatabase()
|
||||
|
||||
sort.Strings(repos)
|
||||
|
||||
@@ -70,13 +70,13 @@ func aptlyMirrorListJSON(_ *commander.Command, _ []string) error {
|
||||
|
||||
repos := make([]*deb.RemoteRepo, context.NewCollectionFactory().RemoteRepoCollection().Len())
|
||||
i := 0
|
||||
_ = context.NewCollectionFactory().RemoteRepoCollection().ForEach(func(repo *deb.RemoteRepo) error {
|
||||
context.NewCollectionFactory().RemoteRepoCollection().ForEach(func(repo *deb.RemoteRepo) error {
|
||||
repos[i] = repo
|
||||
i++
|
||||
return nil
|
||||
})
|
||||
|
||||
_ = context.CloseDatabase()
|
||||
context.CloseDatabase()
|
||||
|
||||
sort.Slice(repos, func(i, j int) bool {
|
||||
return repos[i].Name < repos[j].Name
|
||||
|
||||
+2
-2
@@ -86,7 +86,7 @@ func aptlyMirrorShowTxt(_ *commander.Command, args []string) error {
|
||||
if repo.LastDownloadDate.IsZero() {
|
||||
fmt.Printf("Unable to show package list, mirror hasn't been downloaded yet.\n")
|
||||
} else {
|
||||
_ = ListPackagesRefList(repo.RefList(), collectionFactory)
|
||||
ListPackagesRefList(repo.RefList(), collectionFactory)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ func aptlyMirrorShowJSON(_ *commander.Command, args []string) error {
|
||||
}
|
||||
|
||||
list.PrepareIndex()
|
||||
_ = list.ForEachIndexed(func(p *deb.Package) error {
|
||||
list.ForEachIndexed(func(p *deb.Package) error {
|
||||
repo.Packages = append(repo.Packages, p.GetFullName())
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -101,7 +101,7 @@ func aptlyMirrorUpdate(cmd *commander.Command, args []string) error {
|
||||
err = context.ReOpenDatabase()
|
||||
if err == nil {
|
||||
repo.MarkAsIdle()
|
||||
_ = collectionFactory.RemoteRepoCollection().Update(repo)
|
||||
collectionFactory.RemoteRepoCollection().Update(repo)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -173,7 +173,7 @@ func aptlyMirrorUpdate(cmd *commander.Command, args []string) error {
|
||||
file, e = os.CreateTemp("", task.File.Filename)
|
||||
if e == nil {
|
||||
task.TempDownPath = file.Name()
|
||||
_ = file.Close()
|
||||
file.Close()
|
||||
}
|
||||
}
|
||||
if e != nil {
|
||||
@@ -261,7 +261,7 @@ func aptlyMirrorUpdate(cmd *commander.Command, args []string) error {
|
||||
return fmt.Errorf("unable to update: download errors:\n %s", strings.Join(errors, "\n "))
|
||||
}
|
||||
|
||||
_ = repo.FinalizeDownload(collectionFactory, context.Progress())
|
||||
repo.FinalizeDownload(collectionFactory, context.Progress())
|
||||
err = collectionFactory.RemoteRepoCollection().Update(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
|
||||
@@ -40,7 +40,7 @@ func aptlyPackageSearch(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
|
||||
format := context.Flags().Lookup("format").Value.String()
|
||||
_ = PrintPackageList(result, format, "")
|
||||
PrintPackageList(result, format, "")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
+3
-3
@@ -84,8 +84,8 @@ func aptlyPackageShow(cmd *commander.Command, args []string) error {
|
||||
result := q.Query(collectionFactory.PackageCollection())
|
||||
|
||||
err = result.ForEach(func(p *deb.Package) error {
|
||||
_ = p.Stanza().WriteTo(w, p.IsSource, false, false)
|
||||
_ = w.Flush()
|
||||
p.Stanza().WriteTo(w, p.IsSource, false, false)
|
||||
w.Flush()
|
||||
fmt.Printf("\n")
|
||||
|
||||
if withFiles {
|
||||
@@ -109,7 +109,7 @@ func aptlyPackageShow(cmd *commander.Command, args []string) error {
|
||||
|
||||
if withReferences {
|
||||
fmt.Printf("References to package:\n")
|
||||
_ = printReferencesTo(p, collectionFactory)
|
||||
printReferencesTo(p, collectionFactory)
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -53,7 +53,7 @@ func aptlyPublishListTxt(cmd *commander.Command, _ []string) error {
|
||||
return fmt.Errorf("unable to load list of repos: %s", err)
|
||||
}
|
||||
|
||||
_ = context.CloseDatabase()
|
||||
context.CloseDatabase()
|
||||
|
||||
sort.Strings(published)
|
||||
|
||||
@@ -99,7 +99,7 @@ func aptlyPublishListJSON(_ *commander.Command, _ []string) error {
|
||||
return fmt.Errorf("unable to load list of repos: %s", err)
|
||||
}
|
||||
|
||||
_ = context.CloseDatabase()
|
||||
context.CloseDatabase()
|
||||
|
||||
sort.Slice(repos, func(i, j int) bool {
|
||||
return repos[i].GetPath() < repos[j].GetPath()
|
||||
|
||||
@@ -156,7 +156,7 @@ func aptlyPublishSnapshotOrRepo(cmd *commander.Command, args []string) error {
|
||||
|
||||
duplicate := collectionFactory.PublishedRepoCollection().CheckDuplicate(published)
|
||||
if duplicate != nil {
|
||||
_ = collectionFactory.PublishedRepoCollection().LoadComplete(duplicate, collectionFactory)
|
||||
collectionFactory.PublishedRepoCollection().LoadComplete(duplicate, collectionFactory)
|
||||
return fmt.Errorf("prefix/distribution already used by another published repo: %s", duplicate)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ func aptlyRepoCreate(cmd *commander.Command, args []string) error {
|
||||
repo := deb.NewLocalRepo(args[0], context.Flags().Lookup("comment").Value.String())
|
||||
repo.DefaultDistribution = context.Flags().Lookup("distribution").Value.String()
|
||||
repo.DefaultComponent = context.Flags().Lookup("component").Value.String()
|
||||
repo.LdapGroup = context.Flags().Lookup("ldap-group").Value.String()
|
||||
|
||||
uploadersFile := context.Flags().Lookup("uploaders-file").Value.Get().(string)
|
||||
if uploadersFile != "" {
|
||||
@@ -79,6 +80,7 @@ Example:
|
||||
cmd.Flag.String("distribution", "", "default distribution when publishing")
|
||||
cmd.Flag.String("component", "main", "default component when publishing")
|
||||
cmd.Flag.String("uploaders-file", "", "uploaders.json to be used when including .changes into this repository")
|
||||
cmd.Flag.String("ldap-group", "", "ldap group that owns the repo, leave empty to allow ALL")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ func aptlyRepoEdit(cmd *commander.Command, args []string) error {
|
||||
repo.DefaultComponent = flag.Value.String()
|
||||
case "uploaders-file":
|
||||
uploadersFile = pointer.ToString(flag.Value.String())
|
||||
case "ldap-group":
|
||||
repo.LdapGroup = flag.Value.String()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -82,6 +84,7 @@ Example:
|
||||
cmd.Flag.String("distribution", "", "default distribution when publishing")
|
||||
cmd.Flag.String("component", "", "default component when publishing")
|
||||
cmd.Flag.String("uploaders-file", "", "uploaders.json to be used when including .changes into this repository")
|
||||
cmd.Flag.String("ldap-group", "", "ldap group that owns the repo, leave empty to allow ALL")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
+4
-4
@@ -32,7 +32,7 @@ func aptlyRepoListTxt(cmd *commander.Command, _ []string) error {
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
repos := make([]string, collectionFactory.LocalRepoCollection().Len())
|
||||
i := 0
|
||||
_ = collectionFactory.LocalRepoCollection().ForEach(func(repo *deb.LocalRepo) error {
|
||||
collectionFactory.LocalRepoCollection().ForEach(func(repo *deb.LocalRepo) error {
|
||||
if raw {
|
||||
repos[i] = repo.Name
|
||||
} else {
|
||||
@@ -47,7 +47,7 @@ func aptlyRepoListTxt(cmd *commander.Command, _ []string) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
_ = context.CloseDatabase()
|
||||
context.CloseDatabase()
|
||||
|
||||
sort.Strings(repos)
|
||||
|
||||
@@ -76,7 +76,7 @@ func aptlyRepoListJSON(_ *commander.Command, _ []string) error {
|
||||
|
||||
repos := make([]*deb.LocalRepo, context.NewCollectionFactory().LocalRepoCollection().Len())
|
||||
i := 0
|
||||
_ = context.NewCollectionFactory().LocalRepoCollection().ForEach(func(repo *deb.LocalRepo) error {
|
||||
context.NewCollectionFactory().LocalRepoCollection().ForEach(func(repo *deb.LocalRepo) error {
|
||||
e := context.NewCollectionFactory().LocalRepoCollection().LoadComplete(repo)
|
||||
if e != nil {
|
||||
return e
|
||||
@@ -87,7 +87,7 @@ func aptlyRepoListJSON(_ *commander.Command, _ []string) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
_ = context.CloseDatabase()
|
||||
context.CloseDatabase()
|
||||
|
||||
sort.Slice(repos, func(i, j int) bool {
|
||||
return repos[i].Name < repos[j].Name
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ func aptlyRepoRemove(cmd *commander.Command, args []string) error {
|
||||
return fmt.Errorf("unable to remove: %s", err)
|
||||
}
|
||||
|
||||
_ = toRemove.ForEach(func(p *deb.Package) error {
|
||||
toRemove.ForEach(func(p *deb.Package) error {
|
||||
list.Remove(p)
|
||||
context.Progress().ColoredPrintf("@r[-]@| %s removed", p)
|
||||
return nil
|
||||
|
||||
+2
-1
@@ -45,6 +45,7 @@ func aptlyRepoShowTxt(_ *commander.Command, args []string) error {
|
||||
fmt.Printf("Comment: %s\n", repo.Comment)
|
||||
fmt.Printf("Default Distribution: %s\n", repo.DefaultDistribution)
|
||||
fmt.Printf("Default Component: %s\n", repo.DefaultComponent)
|
||||
fmt.Printf("Ldap Group: %s\n", repo.LdapGroup)
|
||||
if repo.Uploaders != nil {
|
||||
fmt.Printf("Uploaders: %s\n", repo.Uploaders)
|
||||
}
|
||||
@@ -52,7 +53,7 @@ func aptlyRepoShowTxt(_ *commander.Command, args []string) error {
|
||||
|
||||
withPackages := context.Flags().Lookup("with-packages").Value.Get().(bool)
|
||||
if withPackages {
|
||||
_ = ListPackagesRefList(repo.RefList(), collectionFactory)
|
||||
ListPackagesRefList(repo.RefList(), collectionFactory)
|
||||
}
|
||||
|
||||
return err
|
||||
|
||||
+2
-2
@@ -8,8 +8,8 @@ import (
|
||||
"github.com/smira/commander"
|
||||
)
|
||||
|
||||
// RunCommand runs single command starting from root cmd with args, optionally initializing context
|
||||
func RunCommand(cmd *commander.Command, cmdArgs []string, initContext bool) (returnCode int) {
|
||||
// Run runs single command starting from root cmd with args, optionally initializing context
|
||||
func Run(cmd *commander.Command, cmdArgs []string, initContext bool) (returnCode int) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fatal, ok := r.(*ctx.FatalError)
|
||||
|
||||
@@ -33,7 +33,7 @@ func aptlySnapshotListTxt(cmd *commander.Command, _ []string) error {
|
||||
collection := collectionFactory.SnapshotCollection()
|
||||
|
||||
if raw {
|
||||
_ = collection.ForEachSorted(sortMethodString, func(snapshot *deb.Snapshot) error {
|
||||
collection.ForEachSorted(sortMethodString, func(snapshot *deb.Snapshot) error {
|
||||
fmt.Printf("%s\n", snapshot.Name)
|
||||
return nil
|
||||
})
|
||||
@@ -68,7 +68,7 @@ func aptlySnapshotListJSON(cmd *commander.Command, _ []string) error {
|
||||
|
||||
jsonSnapshots := make([]*deb.Snapshot, collection.Len())
|
||||
i := 0
|
||||
_ = collection.ForEachSorted(sortMethodString, func(snapshot *deb.Snapshot) error {
|
||||
collection.ForEachSorted(sortMethodString, func(snapshot *deb.Snapshot) error {
|
||||
jsonSnapshots[i] = snapshot
|
||||
i++
|
||||
return nil
|
||||
|
||||
@@ -116,7 +116,7 @@ func aptlySnapshotPull(cmd *commander.Command, args []string) error {
|
||||
|
||||
alreadySeen := map[string]bool{}
|
||||
|
||||
_ = result.ForEachIndexed(func(pkg *deb.Package) error {
|
||||
result.ForEachIndexed(func(pkg *deb.Package) error {
|
||||
key := pkg.Architecture + "_" + pkg.Name
|
||||
_, seen := alreadySeen[key]
|
||||
|
||||
@@ -132,7 +132,7 @@ func aptlySnapshotPull(cmd *commander.Command, args []string) error {
|
||||
|
||||
// If !allMatches, add only first matching name-arch package
|
||||
if !seen || allMatches {
|
||||
_ = packageList.Add(pkg)
|
||||
packageList.Add(pkg)
|
||||
context.Progress().ColoredPrintf("@g[+]@| %s added", pkg)
|
||||
}
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ func aptlySnapshotMirrorRepoSearch(cmd *commander.Command, args []string) error
|
||||
}
|
||||
|
||||
format := context.Flags().Lookup("format").Value.String()
|
||||
_ = PrintPackageList(result, format, "")
|
||||
PrintPackageList(result, format, "")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ func aptlySnapshotShowTxt(_ *commander.Command, args []string) error {
|
||||
|
||||
withPackages := context.Flags().Lookup("with-packages").Value.Get().(bool)
|
||||
if withPackages {
|
||||
_ = ListPackagesRefList(snapshot.RefList(), collectionFactory)
|
||||
ListPackagesRefList(snapshot.RefList(), collectionFactory)
|
||||
}
|
||||
|
||||
return err
|
||||
@@ -139,7 +139,7 @@ func aptlySnapshotShowJSON(_ *commander.Command, args []string) error {
|
||||
}
|
||||
|
||||
list.PrepareIndex()
|
||||
_ = list.ForEachIndexed(func(p *deb.Package) error {
|
||||
list.ForEachIndexed(func(p *deb.Package) error {
|
||||
snapshot.Packages = append(snapshot.Packages, p.GetFullName())
|
||||
return nil
|
||||
})
|
||||
|
||||
+3
-3
@@ -6,7 +6,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
shellwords "github.com/mattn/go-shellwords"
|
||||
"github.com/mattn/go-shellwords"
|
||||
"github.com/smira/commander"
|
||||
)
|
||||
|
||||
@@ -31,7 +31,7 @@ func aptlyTaskRun(cmd *commander.Command, args []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
|
||||
@@ -89,7 +89,7 @@ func aptlyTaskRun(cmd *commander.Command, args []string) error {
|
||||
context.Progress().ColoredPrintf("\n@yBegin command output: ----------------------------@!")
|
||||
context.Progress().Flush()
|
||||
|
||||
returnCode := RunCommand(RootCommand(), command, false)
|
||||
returnCode := Run(RootCommand(), command, false)
|
||||
if returnCode != 0 {
|
||||
commandErrored = true
|
||||
}
|
||||
|
||||
@@ -242,6 +242,7 @@ local keyring="*-keyring=[gpg keyring to use when verifying Release file (could
|
||||
local create_edit=("-comment=[any text that would be used to described local repository]:comment: "
|
||||
"-component=[default component when publishing]:component:($components)"
|
||||
"-distribution=[default distribution when publishing]:distribution:($dists)"
|
||||
"-ldap-group=[ldap group for repo actions, empty by default]:ldap-group"
|
||||
$aptly_uploaders
|
||||
)
|
||||
|
||||
|
||||
+9
-45
@@ -22,36 +22,34 @@
|
||||
|
||||
__aptly_mirror_list()
|
||||
{
|
||||
aptly ${aptly_global_opts[@]} mirror list -raw
|
||||
aptly mirror list -raw
|
||||
}
|
||||
|
||||
__aptly_repo_list()
|
||||
{
|
||||
aptly ${aptly_global_opts[@]} repo list -raw
|
||||
aptly repo list -raw
|
||||
}
|
||||
|
||||
__aptly_snapshot_list()
|
||||
{
|
||||
aptly ${aptly_global_opts[@]} snapshot list -raw
|
||||
aptly snapshot list -raw
|
||||
}
|
||||
|
||||
__aptly_published_distributions()
|
||||
{
|
||||
aptly ${aptly_global_opts[@]} publish list -raw | cut -d ' ' -f 2 | sort | uniq
|
||||
aptly publish list -raw | cut -d ' ' -f 2 | sort | uniq
|
||||
}
|
||||
|
||||
__aptly_published_prefixes()
|
||||
{
|
||||
aptly ${aptly_global_opts[@]} publish list -raw | cut -d ' ' -f 1 | sort | uniq
|
||||
aptly publish list -raw | cut -d ' ' -f 1 | sort | uniq
|
||||
}
|
||||
|
||||
__aptly_prefixes_for_distribution()
|
||||
{
|
||||
aptly ${aptly_global_opts[@]} publish list -raw | awk -v dist="$1" '{ if (dist == $2) print $1 }' | sort | uniq
|
||||
aptly publish list -raw | awk -v dist="$1" '{ if (dist == $2) print $1 }' | sort | uniq
|
||||
}
|
||||
|
||||
|
||||
|
||||
_aptly()
|
||||
{
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
@@ -59,12 +57,7 @@ _aptly()
|
||||
prevprev="${COMP_WORDS[COMP_CWORD-2]}"
|
||||
|
||||
commands="api config db graph mirror package publish repo serve snapshot task version"
|
||||
|
||||
options="-architectures -config -db-open-attempts -dep-follow-all-variants -dep-follow-recommends -dep-follow-source -dep-follow-suggests -dep-verbose-resolve -gpg-provider"
|
||||
options_without_arg="-dep-follow-all-variants -dep-follow-recommends -dep-follow-source -dep-follow-suggests -dep-verbose-resolve"
|
||||
options_with_arg="-architectures -db-open-attempts -gpg-provider"
|
||||
options_with_path_arg="-config"
|
||||
|
||||
options="-architectures= -config= -db-open-attempts= -dep-follow-all-variants -dep-follow-recommends -dep-follow-source -dep-follow-suggests -dep-verbose-resolve -gpg-provider="
|
||||
db_subcommands="cleanup recover"
|
||||
mirror_subcommands="create drop edit show list rename search update"
|
||||
publish_subcommands="drop list repo snapshot switch update source"
|
||||
@@ -76,41 +69,12 @@ _aptly()
|
||||
config_subcommands="show"
|
||||
api_subcommands="serve"
|
||||
|
||||
local cmd subcmd numargs numoptions i aptly_global_opts
|
||||
local cmd subcmd numargs numoptions i
|
||||
|
||||
numargs=0
|
||||
numoptions=0
|
||||
|
||||
for opt in "${options_with_path_arg[@]}"; do
|
||||
[[ "$prev" == "$opt" ]] || continue
|
||||
compopt -o filenames 2>/dev/null
|
||||
_filedir
|
||||
return 0
|
||||
done
|
||||
|
||||
for (( i=1; i < $COMP_CWORD; i++ )); do
|
||||
word=${COMP_WORDS[i]}
|
||||
if [[ "$word" == -*=* ]]; then
|
||||
for o in "${options[@]}"; do
|
||||
[[ ${word%%=*} == "$o" ]] && aptly_global_opts+=("$word")
|
||||
done
|
||||
else
|
||||
for o in "${options_with_arg[@]}" ""${options_with_path_arg[@]}"" ; do
|
||||
if [[ "$word" == "$o" ]]; then
|
||||
if (( i + 1 < COMP_CWORD )); then
|
||||
aptly_global_opts+=("$word" "${COMP_WORDS[i+1]}")
|
||||
else
|
||||
aptly_global_opts+=("$word")
|
||||
fi
|
||||
(( i++ ))
|
||||
continue 2
|
||||
fi
|
||||
done
|
||||
fi
|
||||
for o in ${options_without_arg[@]}; do
|
||||
[[ "$word" == "$o" ]] && aptly_global_opts+=("$word")
|
||||
done
|
||||
|
||||
if [[ -n "$cmd" ]]; then
|
||||
if [[ ! -n "$subcmd" ]]; then
|
||||
subcmd=${COMP_WORDS[i]}
|
||||
@@ -375,7 +339,7 @@ _aptly()
|
||||
if [[ "$cur" == -* ]]; then
|
||||
COMPREPLY=($(compgen -W "-accept-unsigned -force-replace -ignore-signatures -keyring= -no-remove-files -repo= -uploaders-file=" -- ${cur}))
|
||||
else
|
||||
compopt -o filenames 2>/dev/null
|
||||
comptopt -o filenames 2>/dev/null
|
||||
COMPREPLY=($(compgen -f -- ${cur}))
|
||||
return 0
|
||||
fi
|
||||
|
||||
+6
-39
@@ -4,10 +4,8 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/cheggaaa/pb"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/wsxiaoys/terminal/color"
|
||||
@@ -64,23 +62,8 @@ func (p *Progress) Start() {
|
||||
// Shutdown shuts down progress display
|
||||
func (p *Progress) Shutdown() {
|
||||
p.ShutdownBar()
|
||||
|
||||
// Send stop signal with timeout to prevent hanging
|
||||
select {
|
||||
case p.queue <- printTask{code: codeStop}:
|
||||
// Successfully sent stop signal
|
||||
case <-time.After(1 * time.Second):
|
||||
// Timeout - queue might be full or nil
|
||||
return
|
||||
}
|
||||
|
||||
// Wait for worker to stop with timeout
|
||||
select {
|
||||
case <-p.stopped:
|
||||
// Worker stopped successfully
|
||||
case <-time.After(1 * time.Second):
|
||||
// Timeout - worker might be stuck
|
||||
}
|
||||
p.queue <- printTask{code: codeStop}
|
||||
<-p.stopped
|
||||
}
|
||||
|
||||
// Flush waits for all queued messages to be displayed
|
||||
@@ -95,7 +78,7 @@ func (p *Progress) InitBar(count int64, isBytes bool, _ aptly.BarType) {
|
||||
if p.bar != nil {
|
||||
panic("bar already initialized")
|
||||
}
|
||||
if utils.RunningOnTerminal() {
|
||||
if RunningOnTerminal() {
|
||||
p.bar = pb.New(0)
|
||||
p.bar.Total = count
|
||||
p.bar.NotPrint = true
|
||||
@@ -158,7 +141,7 @@ func (p *Progress) PrintfStdErr(msg string, a ...interface{}) {
|
||||
|
||||
// ColoredPrintf does printf in colored way + newline
|
||||
func (p *Progress) ColoredPrintf(msg string, a ...interface{}) {
|
||||
if utils.RunningOnTerminal() {
|
||||
if RunningOnTerminal() {
|
||||
p.queue <- printTask{code: codePrint, message: color.Sprintf(msg, a...) + "\n"}
|
||||
} else {
|
||||
// stip color marks
|
||||
@@ -217,15 +200,7 @@ func (w *standardProgressWorker) run() {
|
||||
hasBar := false
|
||||
|
||||
for {
|
||||
task, ok := <-w.progress.queue
|
||||
if !ok {
|
||||
// Channel closed, exit gracefully
|
||||
select {
|
||||
case w.progress.stopped <- true:
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
task := <-w.progress.queue
|
||||
switch task.code {
|
||||
case codeBarEnabled:
|
||||
hasBar = true
|
||||
@@ -270,15 +245,7 @@ func (w *loggerProgressWorker) run() {
|
||||
hasBar := false
|
||||
|
||||
for {
|
||||
task, ok := <-w.progress.queue
|
||||
if !ok {
|
||||
// Channel closed, exit gracefully
|
||||
select {
|
||||
case w.progress.stopped <- true:
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
task := <-w.progress.queue
|
||||
switch task.code {
|
||||
case codeBarEnabled:
|
||||
hasBar = true
|
||||
|
||||
@@ -11,7 +11,7 @@ func Test(t *testing.T) {
|
||||
TestingT(t)
|
||||
}
|
||||
|
||||
type ProgressSuite struct{}
|
||||
type ProgressSuite struct {}
|
||||
|
||||
var _ = Suite(&ProgressSuite{})
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package console
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// RunningOnTerminal checks whether stdout is terminal
|
||||
func RunningOnTerminal() bool {
|
||||
return term.IsTerminal(syscall.Stdout)
|
||||
}
|
||||
+21
-83
@@ -115,7 +115,7 @@ func (context *AptlyContext) config() *utils.ConfigStructure {
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Config file not found, creating default config at %s\n\n", homeLocation)
|
||||
|
||||
_ = utils.SaveConfigRaw(homeLocation, aptly.AptlyConf)
|
||||
utils.SaveConfigRaw(homeLocation, aptly.AptlyConf)
|
||||
err = utils.LoadConfig(homeLocation, &utils.Config)
|
||||
if err != nil {
|
||||
Fatal(fmt.Errorf("error loading config file %s: %s", homeLocation, err))
|
||||
@@ -123,14 +123,6 @@ func (context *AptlyContext) config() *utils.ConfigStructure {
|
||||
}
|
||||
}
|
||||
|
||||
if utils.Config.LogFormat == "json" {
|
||||
context.StructuredLogging(true)
|
||||
utils.SetupJSONLogger(utils.Config.LogLevel, os.Stdout)
|
||||
} else {
|
||||
context.StructuredLogging(false)
|
||||
utils.SetupDefaultLogger(utils.Config.LogLevel)
|
||||
}
|
||||
|
||||
context.configLoaded = true
|
||||
|
||||
}
|
||||
@@ -241,7 +233,7 @@ func (context *AptlyContext) newDownloader(progress aptly.Progress) aptly.Downlo
|
||||
// If flag is defined prefer it to global setting
|
||||
maxTries = maxTriesFlag.Value.Get().(int)
|
||||
}
|
||||
var downloader = context.config().Downloader
|
||||
var downloader string = context.config().Downloader
|
||||
downloaderFlag := context.flags.Lookup("downloader")
|
||||
if downloaderFlag != nil {
|
||||
downloader = downloaderFlag.Value.String()
|
||||
@@ -303,41 +295,12 @@ func (context *AptlyContext) _database() (database.Storage, error) {
|
||||
switch context.config().DatabaseBackend.Type {
|
||||
case "leveldb":
|
||||
dbPath := filepath.Join(context.config().GetRootDir(), "db")
|
||||
if len(context.config().DatabaseBackend.DBPath) != 0 {
|
||||
dbPath = context.config().DatabaseBackend.DBPath
|
||||
if len(context.config().DatabaseBackend.DbPath) != 0 {
|
||||
dbPath = context.config().DatabaseBackend.DbPath
|
||||
}
|
||||
context.database, err = goleveldb.NewDB(dbPath)
|
||||
case "etcd":
|
||||
// Configure etcd from config values
|
||||
etcddb.ConfigureFromDBConfig(
|
||||
context.config().DatabaseBackend.Timeout,
|
||||
context.config().DatabaseBackend.WriteRetries,
|
||||
)
|
||||
|
||||
// Create queue config from settings
|
||||
queueConfig := &etcddb.QueueConfig{
|
||||
Enabled: context.config().DatabaseBackend.WriteQueue.Enabled,
|
||||
WriteQueueSize: context.config().DatabaseBackend.WriteQueue.QueueSize,
|
||||
MaxWritesPerSec: context.config().DatabaseBackend.WriteQueue.MaxWritesPerSec,
|
||||
BatchMaxSize: context.config().DatabaseBackend.WriteQueue.BatchMaxSize,
|
||||
BatchMaxWait: time.Duration(context.config().DatabaseBackend.WriteQueue.BatchMaxWaitMs) * time.Millisecond,
|
||||
}
|
||||
|
||||
// Set defaults if not configured
|
||||
if queueConfig.WriteQueueSize == 0 {
|
||||
queueConfig.WriteQueueSize = 1000
|
||||
}
|
||||
if queueConfig.MaxWritesPerSec == 0 {
|
||||
queueConfig.MaxWritesPerSec = 100
|
||||
}
|
||||
if queueConfig.BatchMaxSize == 0 {
|
||||
queueConfig.BatchMaxSize = 50
|
||||
}
|
||||
if queueConfig.BatchMaxWait == 0 {
|
||||
queueConfig.BatchMaxWait = 10 * time.Millisecond
|
||||
}
|
||||
|
||||
context.database, err = etcddb.NewDBWithQueue(context.config().DatabaseBackend.URL, queueConfig)
|
||||
context.database, err = etcddb.NewDB(context.config().DatabaseBackend.URL)
|
||||
default:
|
||||
context.database, err = goleveldb.NewDB(context.dbPath())
|
||||
}
|
||||
@@ -437,42 +400,22 @@ func (context *AptlyContext) PackagePool() aptly.PackagePool {
|
||||
|
||||
// GetPublishedStorage returns instance of PublishedStorage
|
||||
func (context *AptlyContext) GetPublishedStorage(name string) aptly.PublishedStorage {
|
||||
// Fast path: check if already exists without lock
|
||||
context.Lock()
|
||||
publishedStorage, ok := context.publishedStorages[name]
|
||||
context.Unlock()
|
||||
|
||||
if ok {
|
||||
return publishedStorage
|
||||
}
|
||||
|
||||
// Slow path: need to create storage
|
||||
context.Lock()
|
||||
defer context.Unlock()
|
||||
|
||||
// Double-check after acquiring lock
|
||||
publishedStorage, ok = context.publishedStorages[name]
|
||||
if ok {
|
||||
return publishedStorage
|
||||
}
|
||||
|
||||
// Now safe to create new storage
|
||||
if true { // Keep original indentation
|
||||
publishedStorage, ok := context.publishedStorages[name]
|
||||
if !ok {
|
||||
if name == "" {
|
||||
publishedStorage = files.NewPublishedStorage(filepath.Join(context.config().GetRootDir(), "public"), "hardlink", "")
|
||||
} else if strings.HasPrefix(name, "filesystem:") {
|
||||
// Get a safe copy of the map
|
||||
fileSystemRoots := context.config().GetFileSystemPublishRoots()
|
||||
params, ok := fileSystemRoots[name[11:]]
|
||||
params, ok := context.config().FileSystemPublishRoots[name[11:]]
|
||||
if !ok {
|
||||
Fatal(fmt.Errorf("published local storage %v not configured", name[11:]))
|
||||
}
|
||||
|
||||
publishedStorage = files.NewPublishedStorage(params.RootDir, params.LinkMethod, params.VerifyMethod)
|
||||
} else if strings.HasPrefix(name, "s3:") {
|
||||
// Get a safe copy of the map
|
||||
s3Roots := context.config().GetS3PublishRoots()
|
||||
params, ok := s3Roots[name[3:]]
|
||||
params, ok := context.config().S3PublishRoots[name[3:]]
|
||||
if !ok {
|
||||
Fatal(fmt.Errorf("published S3 storage %v not configured", name[3:]))
|
||||
}
|
||||
@@ -482,15 +425,12 @@ func (context *AptlyContext) GetPublishedStorage(name string) aptly.PublishedSto
|
||||
params.AccessKeyID, params.SecretAccessKey, params.SessionToken,
|
||||
params.Region, params.Endpoint, params.Bucket, params.ACL, params.Prefix, params.StorageClass,
|
||||
params.EncryptionMethod, params.PlusWorkaround, params.DisableMultiDel,
|
||||
params.ForceSigV2, params.ForceVirtualHostedStyle, params.Debug, params.ConcurrentUploads,
|
||||
params.UploadQueueSize)
|
||||
params.ForceSigV2, params.ForceVirtualHostedStyle, params.Debug)
|
||||
if err != nil {
|
||||
Fatal(err)
|
||||
}
|
||||
} else if strings.HasPrefix(name, "swift:") {
|
||||
// Get a safe copy of the map
|
||||
swiftRoots := context.config().GetSwiftPublishRoots()
|
||||
params, ok := swiftRoots[name[6:]]
|
||||
params, ok := context.config().SwiftPublishRoots[name[6:]]
|
||||
if !ok {
|
||||
Fatal(fmt.Errorf("published Swift storage %v not configured", name[6:]))
|
||||
}
|
||||
@@ -502,11 +442,9 @@ func (context *AptlyContext) GetPublishedStorage(name string) aptly.PublishedSto
|
||||
Fatal(err)
|
||||
}
|
||||
} else if strings.HasPrefix(name, "azure:") {
|
||||
// Get a safe copy of the map
|
||||
azureRoots := context.config().GetAzurePublishRoots()
|
||||
params, ok := azureRoots[name[6:]]
|
||||
params, ok := context.config().AzurePublishRoots[name[6:]]
|
||||
if !ok {
|
||||
Fatal(fmt.Errorf("published Azure storage %v not configured", name[6:]))
|
||||
Fatal(fmt.Errorf("Published Azure storage %v not configured", name[6:]))
|
||||
}
|
||||
|
||||
var err error
|
||||
@@ -651,17 +589,17 @@ func (context *AptlyContext) Shutdown() {
|
||||
|
||||
if aptly.EnableDebug {
|
||||
if context.fileMemProfile != nil {
|
||||
_ = pprof.WriteHeapProfile(context.fileMemProfile)
|
||||
_ = context.fileMemProfile.Close()
|
||||
pprof.WriteHeapProfile(context.fileMemProfile)
|
||||
context.fileMemProfile.Close()
|
||||
context.fileMemProfile = nil
|
||||
}
|
||||
if context.fileCPUProfile != nil {
|
||||
pprof.StopCPUProfile()
|
||||
_ = context.fileCPUProfile.Close()
|
||||
context.fileCPUProfile.Close()
|
||||
context.fileCPUProfile = nil
|
||||
}
|
||||
if context.fileMemProfile != nil {
|
||||
_ = context.fileMemProfile.Close()
|
||||
context.fileMemProfile.Close()
|
||||
context.fileMemProfile = nil
|
||||
}
|
||||
}
|
||||
@@ -669,7 +607,7 @@ func (context *AptlyContext) Shutdown() {
|
||||
context.taskList.Stop()
|
||||
}
|
||||
if context.database != nil {
|
||||
_ = context.database.Close()
|
||||
context.database.Close()
|
||||
context.database = nil
|
||||
}
|
||||
if context.downloader != nil {
|
||||
@@ -714,7 +652,7 @@ func NewContext(flags *flag.FlagSet) (*AptlyContext, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = pprof.StartCPUProfile(context.fileCPUProfile)
|
||||
pprof.StartCPUProfile(context.fileCPUProfile)
|
||||
}
|
||||
|
||||
memprofile := flags.Lookup("memprofile").Value.String()
|
||||
@@ -734,7 +672,7 @@ func NewContext(flags *flag.FlagSet) (*AptlyContext, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, _ = context.fileMemStats.WriteString("# Time\tHeapSys\tHeapAlloc\tHeapIdle\tHeapReleased\n")
|
||||
context.fileMemStats.WriteString("# Time\tHeapSys\tHeapAlloc\tHeapIdle\tHeapReleased\n")
|
||||
|
||||
go func() {
|
||||
var stats runtime.MemStats
|
||||
@@ -744,7 +682,7 @@ func NewContext(flags *flag.FlagSet) (*AptlyContext, error) {
|
||||
for {
|
||||
runtime.ReadMemStats(&stats)
|
||||
if context.fileMemStats != nil {
|
||||
_, _ = context.fileMemStats.WriteString(fmt.Sprintf("%d\t%d\t%d\t%d\t%d\n",
|
||||
context.fileMemStats.WriteString(fmt.Sprintf("%d\t%d\t%d\t%d\t%d\n",
|
||||
(time.Now().UnixNano()-start)/1000000, stats.HeapSys, stats.HeapAlloc, stats.HeapIdle, stats.HeapReleased))
|
||||
time.Sleep(interval)
|
||||
} else {
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
)
|
||||
|
||||
// Test for unsafe map access race condition
|
||||
func TestPublishedStorageMapRace(t *testing.T) {
|
||||
// Create a context with empty config
|
||||
context := &AptlyContext{
|
||||
publishedStorages: make(map[string]aptly.PublishedStorage),
|
||||
configLoaded: true, // Skip config loading
|
||||
}
|
||||
|
||||
// Mock config
|
||||
utils.Config = utils.ConfigStructure{
|
||||
RootDir: "/tmp/aptly-test",
|
||||
FileSystemPublishRoots: map[string]utils.FileSystemPublishRoot{
|
||||
"test": {RootDir: "/tmp/test", LinkMethod: "hardlink"},
|
||||
},
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errors := make(chan error, 100)
|
||||
|
||||
// Simulate concurrent access to the same storage
|
||||
for i := 0; i < 50; i++ {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
errors <- fmt.Errorf("panic in goroutine %d: %v", id, r)
|
||||
}
|
||||
}()
|
||||
|
||||
// All goroutines try to access the same storage
|
||||
storage := context.GetPublishedStorage("filesystem:test")
|
||||
if storage == nil {
|
||||
errors <- fmt.Errorf("got nil storage in goroutine %d", id)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Also test different storages to trigger map growth
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
errors <- fmt.Errorf("panic in storage %d: %v", id, r)
|
||||
}
|
||||
}()
|
||||
|
||||
// Add new storage configurations
|
||||
storageName := fmt.Sprintf("filesystem:test%d", id)
|
||||
utils.Config.SetFileSystemPublishRoot(fmt.Sprintf("test%d", id), utils.FileSystemPublishRoot{
|
||||
RootDir: fmt.Sprintf("/tmp/test%d", id),
|
||||
LinkMethod: "hardlink",
|
||||
})
|
||||
|
||||
storage := context.GetPublishedStorage(storageName)
|
||||
if storage == nil {
|
||||
errors <- fmt.Errorf("got nil storage for %s", storageName)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errors)
|
||||
|
||||
// Check for any errors or panics
|
||||
for err := range errors {
|
||||
t.Errorf("Race condition error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Test for concurrent map writes
|
||||
func TestPublishedStorageConcurrentWrites(t *testing.T) {
|
||||
context := &AptlyContext{
|
||||
publishedStorages: make(map[string]aptly.PublishedStorage),
|
||||
configLoaded: true, // Skip config loading
|
||||
}
|
||||
|
||||
utils.Config = utils.ConfigStructure{
|
||||
RootDir: "/tmp/aptly-test",
|
||||
FileSystemPublishRoots: make(map[string]utils.FileSystemPublishRoot),
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
panics := make(chan string, 100)
|
||||
|
||||
// Multiple goroutines trying to create different storages simultaneously
|
||||
for i := 0; i < 20; i++ {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
panics <- fmt.Sprintf("goroutine %d panicked: %v", id, r)
|
||||
}
|
||||
}()
|
||||
|
||||
storageName := fmt.Sprintf("filesystem:concurrent%d", id)
|
||||
utils.Config.SetFileSystemPublishRoot(fmt.Sprintf("concurrent%d", id), utils.FileSystemPublishRoot{
|
||||
RootDir: fmt.Sprintf("/tmp/concurrent%d", id),
|
||||
LinkMethod: "hardlink",
|
||||
})
|
||||
|
||||
// This should trigger concurrent map writes
|
||||
_ = context.GetPublishedStorage(storageName)
|
||||
|
||||
// Add some delay to increase chance of race
|
||||
time.Sleep(time.Millisecond)
|
||||
|
||||
// Access again to ensure consistency
|
||||
storage2 := context.GetPublishedStorage(storageName)
|
||||
if storage2 == nil {
|
||||
panics <- fmt.Sprintf("inconsistent storage access in goroutine %d", id)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(panics)
|
||||
|
||||
// Check for panics (indicating race condition)
|
||||
for panic := range panics {
|
||||
t.Errorf("Concurrent map access issue: %s", panic)
|
||||
}
|
||||
}
|
||||
|
||||
// Test for storage initialization race
|
||||
func TestPublishedStorageInitRace(t *testing.T) {
|
||||
// Run this test multiple times to increase chance of catching race
|
||||
for attempt := 0; attempt < 10; attempt++ {
|
||||
context := &AptlyContext{
|
||||
publishedStorages: make(map[string]aptly.PublishedStorage),
|
||||
configLoaded: true, // Skip config loading
|
||||
}
|
||||
|
||||
utils.Config = utils.ConfigStructure{
|
||||
RootDir: "/tmp/aptly-test",
|
||||
FileSystemPublishRoots: map[string]utils.FileSystemPublishRoot{
|
||||
"race": {RootDir: "/tmp/race", LinkMethod: "hardlink"},
|
||||
},
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
storages := make([]aptly.PublishedStorage, 10)
|
||||
|
||||
// Multiple goroutines accessing the same non-existent storage
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
storages[idx] = context.GetPublishedStorage("filesystem:race")
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// All should get the same storage instance
|
||||
firstStorage := storages[0]
|
||||
for i := 1; i < len(storages); i++ {
|
||||
if storages[i] != firstStorage {
|
||||
t.Errorf("Attempt %d: Got different storage instances: race condition in initialization", attempt)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
)
|
||||
|
||||
func TestQueueConfigurationParsing(t *testing.T) {
|
||||
// Test default configuration
|
||||
config := utils.ConfigStructure{
|
||||
DatabaseBackend: utils.DBConfig{
|
||||
Type: "etcd",
|
||||
URL: "localhost:2379",
|
||||
},
|
||||
}
|
||||
|
||||
// Verify defaults are applied
|
||||
if config.DatabaseBackend.WriteQueue.Enabled {
|
||||
t.Error("Expected write queue to be disabled by default")
|
||||
}
|
||||
|
||||
// Test with explicit configuration
|
||||
config.DatabaseBackend.WriteQueue = utils.WriteQConfig{
|
||||
Enabled: true,
|
||||
QueueSize: 500,
|
||||
MaxWritesPerSec: 50,
|
||||
BatchMaxSize: 25,
|
||||
BatchMaxWaitMs: 20,
|
||||
}
|
||||
|
||||
if !config.DatabaseBackend.WriteQueue.Enabled {
|
||||
t.Error("Expected write queue to be enabled")
|
||||
}
|
||||
if config.DatabaseBackend.WriteQueue.QueueSize != 500 {
|
||||
t.Errorf("Expected queue size 500, got %d", config.DatabaseBackend.WriteQueue.QueueSize)
|
||||
}
|
||||
if config.DatabaseBackend.WriteQueue.MaxWritesPerSec != 50 {
|
||||
t.Errorf("Expected max writes per sec 50, got %d", config.DatabaseBackend.WriteQueue.MaxWritesPerSec)
|
||||
}
|
||||
if config.DatabaseBackend.WriteQueue.BatchMaxSize != 25 {
|
||||
t.Errorf("Expected batch max size 25, got %d", config.DatabaseBackend.WriteQueue.BatchMaxSize)
|
||||
}
|
||||
if config.DatabaseBackend.WriteQueue.BatchMaxWaitMs != 20 {
|
||||
t.Errorf("Expected batch max wait 20ms, got %d", config.DatabaseBackend.WriteQueue.BatchMaxWaitMs)
|
||||
}
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
check "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
// Hook up gocheck into the "go test" runner.
|
||||
func Test(t *testing.T) { check.TestingT(t) }
|
||||
|
||||
type DatabaseSuite struct{}
|
||||
|
||||
var _ = check.Suite(&DatabaseSuite{})
|
||||
|
||||
func (s *DatabaseSuite) TestErrNotFound(c *check.C) {
|
||||
// Test that ErrNotFound is properly defined
|
||||
c.Check(ErrNotFound, check.NotNil)
|
||||
c.Check(ErrNotFound.Error(), check.Equals, "key not found")
|
||||
|
||||
// Test that it's an actual error
|
||||
var err error = ErrNotFound
|
||||
c.Check(err, check.NotNil)
|
||||
|
||||
// Test comparison with errors.New
|
||||
newErr := errors.New("key not found")
|
||||
c.Check(ErrNotFound.Error(), check.Equals, newErr.Error())
|
||||
|
||||
// Test that it's not equal to other errors
|
||||
otherErr := errors.New("other error")
|
||||
c.Check(ErrNotFound.Error(), check.Not(check.Equals), otherErr.Error())
|
||||
}
|
||||
|
||||
func (s *DatabaseSuite) TestStorageProcessor(c *check.C) {
|
||||
// Test StorageProcessor function type
|
||||
called := false
|
||||
var processor StorageProcessor = func(key []byte, value []byte) error {
|
||||
called = true
|
||||
c.Check(key, check.DeepEquals, []byte("test-key"))
|
||||
c.Check(value, check.DeepEquals, []byte("test-value"))
|
||||
return nil
|
||||
}
|
||||
|
||||
err := processor([]byte("test-key"), []byte("test-value"))
|
||||
c.Check(err, check.IsNil)
|
||||
c.Check(called, check.Equals, true)
|
||||
}
|
||||
|
||||
func (s *DatabaseSuite) TestStorageProcessorWithError(c *check.C) {
|
||||
// Test StorageProcessor that returns an error
|
||||
testError := errors.New("processing error")
|
||||
var processor StorageProcessor = func(key []byte, value []byte) error {
|
||||
return testError
|
||||
}
|
||||
|
||||
err := processor([]byte("key"), []byte("value"))
|
||||
c.Check(err, check.Equals, testError)
|
||||
}
|
||||
|
||||
func (s *DatabaseSuite) TestStorageProcessorNilInputs(c *check.C) {
|
||||
// Test StorageProcessor with nil inputs
|
||||
var processor StorageProcessor = func(key []byte, value []byte) error {
|
||||
c.Check(key, check.IsNil)
|
||||
c.Check(value, check.DeepEquals, []byte("value"))
|
||||
return nil
|
||||
}
|
||||
|
||||
err := processor(nil, []byte("value"))
|
||||
c.Check(err, check.IsNil)
|
||||
}
|
||||
|
||||
func (s *DatabaseSuite) TestStorageProcessorEmptyInputs(c *check.C) {
|
||||
// Test StorageProcessor with empty inputs
|
||||
var processor StorageProcessor = func(key []byte, value []byte) error {
|
||||
c.Check(len(key), check.Equals, 0)
|
||||
c.Check(len(value), check.Equals, 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
err := processor([]byte{}, []byte{})
|
||||
c.Check(err, check.IsNil)
|
||||
}
|
||||
|
||||
// Mock implementations to test interface compliance
|
||||
type mockReader struct {
|
||||
data map[string][]byte
|
||||
}
|
||||
|
||||
func (m *mockReader) Get(key []byte) ([]byte, error) {
|
||||
if value, exists := m.data[string(key)]; exists {
|
||||
return value, nil
|
||||
}
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
type mockWriter struct {
|
||||
data map[string][]byte
|
||||
}
|
||||
|
||||
func (m *mockWriter) Put(key []byte, value []byte) error {
|
||||
m.data[string(key)] = value
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockWriter) Delete(key []byte) error {
|
||||
delete(m.data, string(key))
|
||||
return nil
|
||||
}
|
||||
|
||||
type mockReaderWriter struct {
|
||||
*mockReader
|
||||
*mockWriter
|
||||
}
|
||||
|
||||
func (s *DatabaseSuite) TestReaderInterface(c *check.C) {
|
||||
// Test Reader interface implementation
|
||||
data := map[string][]byte{
|
||||
"key1": []byte("value1"),
|
||||
"key2": []byte("value2"),
|
||||
}
|
||||
|
||||
var reader Reader = &mockReader{data: data}
|
||||
|
||||
// Test existing key
|
||||
value, err := reader.Get([]byte("key1"))
|
||||
c.Check(err, check.IsNil)
|
||||
c.Check(value, check.DeepEquals, []byte("value1"))
|
||||
|
||||
// Test non-existing key
|
||||
value, err = reader.Get([]byte("nonexistent"))
|
||||
c.Check(err, check.Equals, ErrNotFound)
|
||||
c.Check(value, check.IsNil)
|
||||
}
|
||||
|
||||
func (s *DatabaseSuite) TestWriterInterface(c *check.C) {
|
||||
// Test Writer interface implementation
|
||||
data := make(map[string][]byte)
|
||||
var writer Writer = &mockWriter{data: data}
|
||||
|
||||
// Test Put
|
||||
err := writer.Put([]byte("key1"), []byte("value1"))
|
||||
c.Check(err, check.IsNil)
|
||||
c.Check(data["key1"], check.DeepEquals, []byte("value1"))
|
||||
|
||||
// Test Delete
|
||||
err = writer.Delete([]byte("key1"))
|
||||
c.Check(err, check.IsNil)
|
||||
_, exists := data["key1"]
|
||||
c.Check(exists, check.Equals, false)
|
||||
}
|
||||
|
||||
func (s *DatabaseSuite) TestReaderWriterInterface(c *check.C) {
|
||||
// Test ReaderWriter interface implementation
|
||||
data := make(map[string][]byte)
|
||||
|
||||
var rw ReaderWriter = &mockReaderWriter{
|
||||
mockReader: &mockReader{data: data},
|
||||
mockWriter: &mockWriter{data: data},
|
||||
}
|
||||
|
||||
// Test write then read
|
||||
err := rw.Put([]byte("test"), []byte("value"))
|
||||
c.Check(err, check.IsNil)
|
||||
|
||||
value, err := rw.Get([]byte("test"))
|
||||
c.Check(err, check.IsNil)
|
||||
c.Check(value, check.DeepEquals, []byte("value"))
|
||||
|
||||
// Test delete
|
||||
err = rw.Delete([]byte("test"))
|
||||
c.Check(err, check.IsNil)
|
||||
|
||||
value, err = rw.Get([]byte("test"))
|
||||
c.Check(err, check.Equals, ErrNotFound)
|
||||
c.Check(value, check.IsNil)
|
||||
}
|
||||
|
||||
// Test that all interfaces are properly defined
|
||||
func (s *DatabaseSuite) TestInterfaceDefinitions(c *check.C) {
|
||||
// This test ensures that all interfaces are properly defined
|
||||
// and can be used as interface types
|
||||
|
||||
var reader Reader
|
||||
var prefixReader PrefixReader
|
||||
var writer Writer
|
||||
var readerWriter ReaderWriter
|
||||
var storage Storage
|
||||
var batch Batch
|
||||
var transaction Transaction
|
||||
|
||||
// Test that they are nil by default
|
||||
c.Check(reader, check.IsNil)
|
||||
c.Check(prefixReader, check.IsNil)
|
||||
c.Check(writer, check.IsNil)
|
||||
c.Check(readerWriter, check.IsNil)
|
||||
c.Check(storage, check.IsNil)
|
||||
c.Check(batch, check.IsNil)
|
||||
c.Check(transaction, check.IsNil)
|
||||
}
|
||||
|
||||
func (s *DatabaseSuite) TestErrorConstants(c *check.C) {
|
||||
// Test that error constants are immutable and consistently defined
|
||||
original := ErrNotFound
|
||||
c.Check(original, check.NotNil)
|
||||
|
||||
// Verify it maintains its identity
|
||||
c.Check(ErrNotFound, check.Equals, original)
|
||||
c.Check(ErrNotFound.Error(), check.Equals, original.Error())
|
||||
}
|
||||
+7
-111
@@ -1,16 +1,8 @@
|
||||
package etcddb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/aptly-dev/aptly/database"
|
||||
"github.com/rs/zerolog/log"
|
||||
clientv3 "go.etcd.io/etcd/client/v3"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type EtcDBatch struct {
|
||||
@@ -34,121 +26,25 @@ func (b *EtcDBatch) Delete(key []byte) (err error) {
|
||||
}
|
||||
|
||||
func (b *EtcDBatch) Write() (err error) {
|
||||
var kv clientv3.KV
|
||||
if b.s.queuedKV != nil {
|
||||
kv = b.s.queuedKV
|
||||
} else {
|
||||
kv = clientv3.NewKV(b.s.db)
|
||||
}
|
||||
kv := clientv3.NewKV(b.s.db)
|
||||
|
||||
batchSize := 128
|
||||
for i := 0; i < len(b.ops); i += batchSize {
|
||||
txn := kv.Txn(Ctx)
|
||||
end := i + batchSize
|
||||
if end > len(b.ops) {
|
||||
end = len(b.ops)
|
||||
}
|
||||
|
||||
batch := b.ops[i:end]
|
||||
|
||||
// Retry logic with exponential backoff
|
||||
var lastErr error
|
||||
for retry := 0; retry <= DefaultWriteRetries; retry++ {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), DefaultTimeout)
|
||||
txn := kv.Txn(ctx)
|
||||
txn.Then(batch...)
|
||||
_, err = txn.Commit()
|
||||
cancel()
|
||||
|
||||
if err == nil {
|
||||
// Success, move to next batch
|
||||
break
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
|
||||
// Check if error is retryable
|
||||
if !isRetryableError(err) {
|
||||
log.Error().Err(err).Int("batch_start", i).Int("batch_end", end).Msg("etcd: non-retryable error during batch write")
|
||||
return fmt.Errorf("etcd batch write failed: %w", err)
|
||||
}
|
||||
|
||||
if retry < DefaultWriteRetries {
|
||||
// Calculate exponential backoff
|
||||
backoff := time.Duration(math.Pow(2, float64(retry))) * 100 * time.Millisecond
|
||||
if backoff > 5*time.Second {
|
||||
backoff = 5 * time.Second
|
||||
}
|
||||
|
||||
log.Warn().Err(err).
|
||||
Int("retry", retry+1).
|
||||
Int("max_retries", DefaultWriteRetries).
|
||||
Dur("backoff", backoff).
|
||||
Int("batch_start", i).
|
||||
Int("batch_end", end).
|
||||
Msg("etcd: batch write failed, retrying")
|
||||
|
||||
time.Sleep(backoff)
|
||||
}
|
||||
}
|
||||
|
||||
// All retries exhausted
|
||||
if lastErr != nil {
|
||||
log.Error().Err(lastErr).
|
||||
Int("batch_start", i).
|
||||
Int("batch_end", end).
|
||||
Int("retries", DefaultWriteRetries).
|
||||
Msg("etcd: batch write failed after all retries")
|
||||
return fmt.Errorf("etcd batch write failed after %d retries: %w", DefaultWriteRetries, lastErr)
|
||||
txn.Then(batch...)
|
||||
_, err = txn.Commit()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isRetryableError checks if an error is retryable
|
||||
func isRetryableError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for gRPC status errors
|
||||
if s, ok := status.FromError(err); ok {
|
||||
switch s.Code() {
|
||||
case codes.Unavailable, codes.DeadlineExceeded, codes.ResourceExhausted, codes.Aborted:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check for context errors
|
||||
if err == context.DeadlineExceeded || err == context.Canceled {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for timeout errors in error message
|
||||
if errStr := err.Error(); errStr != "" {
|
||||
if contains(errStr, "timeout") || contains(errStr, "timed out") ||
|
||||
contains(errStr, "unavailable") || contains(errStr, "connection refused") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// contains is a simple string contains helper
|
||||
func contains(s, substr string) bool {
|
||||
return len(substr) > 0 && len(s) >= len(substr) &&
|
||||
(s == substr || s[:len(substr)] == substr || s[len(s)-len(substr):] == substr ||
|
||||
len(s) > len(substr) && findSubstring(s, substr))
|
||||
}
|
||||
|
||||
func findSubstring(s, substr string) bool {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return
|
||||
}
|
||||
|
||||
// batch should implement database.Batch
|
||||
|
||||
+7
-117
@@ -1,83 +1,24 @@
|
||||
package etcddb
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/aptly-dev/aptly/database"
|
||||
"github.com/rs/zerolog/log"
|
||||
clientv3 "go.etcd.io/etcd/client/v3"
|
||||
)
|
||||
|
||||
// Default timeout for etcd operations
|
||||
var DefaultTimeout = 60 * time.Second
|
||||
|
||||
// Default write retry count
|
||||
var DefaultWriteRetries = 3
|
||||
|
||||
func init() {
|
||||
// Allow timeout configuration via environment variable
|
||||
if timeout := os.Getenv("APTLY_ETCD_TIMEOUT"); timeout != "" {
|
||||
if d, err := time.ParseDuration(timeout); err == nil {
|
||||
DefaultTimeout = d
|
||||
log.Info().Dur("timeout", d).Msg("etcd: using custom timeout")
|
||||
} else {
|
||||
log.Warn().Str("value", timeout).Err(err).Msg("etcd: invalid timeout value, using default")
|
||||
}
|
||||
}
|
||||
|
||||
// Allow write retry configuration via environment variable
|
||||
if retries := os.Getenv("APTLY_ETCD_WRITE_RETRIES"); retries != "" {
|
||||
if r, err := strconv.Atoi(retries); err == nil && r >= 0 {
|
||||
DefaultWriteRetries = r
|
||||
log.Info().Int("retries", r).Msg("etcd: using custom write retry count")
|
||||
} else {
|
||||
log.Warn().Str("value", retries).Err(err).Msg("etcd: invalid write retry value, using default")
|
||||
}
|
||||
}
|
||||
}
|
||||
var Ctx = context.TODO()
|
||||
|
||||
func internalOpen(url string) (cli *clientv3.Client, err error) {
|
||||
// Configure dial timeout
|
||||
dialTimeout := 60 * time.Second
|
||||
if dt := os.Getenv("APTLY_ETCD_DIAL_TIMEOUT"); dt != "" {
|
||||
if d, err := time.ParseDuration(dt); err == nil {
|
||||
dialTimeout = d
|
||||
}
|
||||
}
|
||||
|
||||
// Configure keep alive timeout
|
||||
keepAliveTimeout := 7200 * time.Second
|
||||
if ka := os.Getenv("APTLY_ETCD_KEEPALIVE"); ka != "" {
|
||||
if d, err := time.ParseDuration(ka); err == nil {
|
||||
keepAliveTimeout = d
|
||||
}
|
||||
}
|
||||
|
||||
// Configure message size
|
||||
maxMsgSize := 50 * 1024 * 1024 // 50MiB default
|
||||
if size := os.Getenv("APTLY_ETCD_MAX_MSG_SIZE"); size != "" {
|
||||
if s, err := strconv.Atoi(size); err == nil && s > 0 {
|
||||
maxMsgSize = s
|
||||
}
|
||||
}
|
||||
|
||||
cfg := clientv3.Config{
|
||||
Endpoints: []string{url},
|
||||
DialTimeout: dialTimeout,
|
||||
MaxCallSendMsgSize: maxMsgSize,
|
||||
MaxCallRecvMsgSize: maxMsgSize,
|
||||
DialKeepAliveTimeout: keepAliveTimeout,
|
||||
DialTimeout: 30 * time.Second,
|
||||
MaxCallSendMsgSize: 2147483647, // (2048 * 1024 * 1024) - 1
|
||||
MaxCallRecvMsgSize: 2147483647,
|
||||
DialKeepAliveTimeout: 7200 * time.Second,
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Str("endpoint", url).
|
||||
Dur("dialTimeout", dialTimeout).
|
||||
Dur("keepAlive", keepAliveTimeout).
|
||||
Int("maxMsgSize", maxMsgSize).
|
||||
Msg("etcd: opening connection")
|
||||
|
||||
cli, err = clientv3.New(cfg)
|
||||
return
|
||||
}
|
||||
@@ -87,56 +28,5 @@ func NewDB(url string) (database.Storage, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &EtcDStorage{
|
||||
url: url,
|
||||
db: cli,
|
||||
queuedClient: nil,
|
||||
queuedKV: nil,
|
||||
tmpPrefix: "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewDBWithQueue creates a new DB with optional write queue
|
||||
func NewDBWithQueue(url string, queueConfig *QueueConfig) (database.Storage, error) {
|
||||
cli, err := internalOpen(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
storage := &EtcDStorage{
|
||||
url: url,
|
||||
db: cli,
|
||||
tmpPrefix: "",
|
||||
}
|
||||
|
||||
if queueConfig != nil && queueConfig.Enabled {
|
||||
storage.queuedClient = NewQueuedEtcdClient(cli, queueConfig)
|
||||
storage.queuedKV = NewQueuedKV(cli.KV, storage.queuedClient.writeQueue, queueConfig)
|
||||
log.Info().
|
||||
Bool("enabled", queueConfig.Enabled).
|
||||
Int("queueSize", queueConfig.WriteQueueSize).
|
||||
Int("maxWritesPerSec", queueConfig.MaxWritesPerSec).
|
||||
Msg("etcd: write queue enabled")
|
||||
}
|
||||
|
||||
return storage, nil
|
||||
}
|
||||
|
||||
// ConfigureFromDBConfig applies configuration from DBConfig
|
||||
func ConfigureFromDBConfig(timeout string, writeRetries int) {
|
||||
// Configure timeout if provided
|
||||
if timeout != "" {
|
||||
if d, err := time.ParseDuration(timeout); err == nil {
|
||||
DefaultTimeout = d
|
||||
log.Info().Dur("timeout", d).Msg("etcd: configured timeout from config")
|
||||
} else {
|
||||
log.Warn().Str("value", timeout).Err(err).Msg("etcd: invalid timeout in config, keeping current value")
|
||||
}
|
||||
}
|
||||
|
||||
// Configure write retries if provided
|
||||
if writeRetries > 0 {
|
||||
DefaultWriteRetries = writeRetries
|
||||
log.Info().Int("retries", writeRetries).Msg("etcd: configured write retries from config")
|
||||
}
|
||||
return &EtcDStorage{url, cli, ""}, nil
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ func Test(t *testing.T) {
|
||||
}
|
||||
|
||||
type EtcDDBSuite struct {
|
||||
db database.Storage
|
||||
url string
|
||||
db database.Storage
|
||||
}
|
||||
|
||||
var _ = Suite(&EtcDDBSuite{})
|
||||
@@ -66,17 +67,17 @@ func (s *EtcDDBSuite) TestDelete(c *C) {
|
||||
func (s *EtcDDBSuite) TestByPrefix(c *C) {
|
||||
//c.Check(s.db.FetchByPrefix([]byte{0x80}), DeepEquals, [][]byte{})
|
||||
|
||||
_ = s.db.Put([]byte{0x80, 0x01}, []byte{0x01})
|
||||
_ = s.db.Put([]byte{0x80, 0x03}, []byte{0x03})
|
||||
_ = s.db.Put([]byte{0x80, 0x02}, []byte{0x02})
|
||||
s.db.Put([]byte{0x80, 0x01}, []byte{0x01})
|
||||
s.db.Put([]byte{0x80, 0x03}, []byte{0x03})
|
||||
s.db.Put([]byte{0x80, 0x02}, []byte{0x02})
|
||||
c.Check(s.db.FetchByPrefix([]byte{0x80}), DeepEquals, [][]byte{{0x01}, {0x02}, {0x03}})
|
||||
c.Check(s.db.KeysByPrefix([]byte{0x80}), DeepEquals, [][]byte{{0x80, 0x01}, {0x80, 0x02}, {0x80, 0x03}})
|
||||
|
||||
_ = s.db.Put([]byte{0x90, 0x01}, []byte{0x04})
|
||||
s.db.Put([]byte{0x90, 0x01}, []byte{0x04})
|
||||
c.Check(s.db.FetchByPrefix([]byte{0x80}), DeepEquals, [][]byte{{0x01}, {0x02}, {0x03}})
|
||||
c.Check(s.db.KeysByPrefix([]byte{0x80}), DeepEquals, [][]byte{{0x80, 0x01}, {0x80, 0x02}, {0x80, 0x03}})
|
||||
|
||||
_ = s.db.Put([]byte{0x00, 0x01}, []byte{0x05})
|
||||
s.db.Put([]byte{0x00, 0x01}, []byte{0x05})
|
||||
c.Check(s.db.FetchByPrefix([]byte{0x80}), DeepEquals, [][]byte{{0x01}, {0x02}, {0x03}})
|
||||
c.Check(s.db.KeysByPrefix([]byte{0x80}), DeepEquals, [][]byte{{0x80, 0x01}, {0x80, 0x02}, {0x80, 0x03}})
|
||||
|
||||
@@ -108,7 +109,7 @@ func (s *EtcDDBSuite) TestHasPrefix(c *C) {
|
||||
//c.Check(s.db.HasPrefix([]byte(nil)), Equals, false)
|
||||
//c.Check(s.db.HasPrefix([]byte{0x80}), Equals, false)
|
||||
|
||||
_ = s.db.Put([]byte{0x80, 0x01}, []byte{0x01})
|
||||
s.db.Put([]byte{0x80, 0x01}, []byte{0x01})
|
||||
|
||||
c.Check(s.db.HasPrefix([]byte(nil)), Equals, true)
|
||||
c.Check(s.db.HasPrefix([]byte{0x80}), Equals, true)
|
||||
@@ -123,17 +124,15 @@ func (s *EtcDDBSuite) TestTransactionCommit(c *C) {
|
||||
value2 = []byte("value2")
|
||||
)
|
||||
transaction, err := s.db.OpenTransaction()
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = s.db.Put(key, value)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
c.Assert(err, IsNil)
|
||||
_ = transaction.Put(key2, value2)
|
||||
transaction.Put(key2, value2)
|
||||
v, err := s.db.Get(key)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(v, DeepEquals, value)
|
||||
err = transaction.Delete(key)
|
||||
err = transaction.Delete(key)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
_, err = transaction.Get(key2)
|
||||
@@ -156,3 +155,4 @@ func (s *EtcDDBSuite) TestTransactionCommit(c *C) {
|
||||
_, err = transaction.Get(key)
|
||||
c.Assert(err, NotNil)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
package etcddb
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
"github.com/aptly-dev/aptly/database"
|
||||
)
|
||||
|
||||
func TestEtcdWithQueue(t *testing.T) {
|
||||
// Test with queue enabled
|
||||
config := &QueueConfig{
|
||||
Enabled: true,
|
||||
WriteQueueSize: 100,
|
||||
MaxWritesPerSec: 100,
|
||||
BatchMaxSize: 10,
|
||||
BatchMaxWait: 10 * time.Millisecond,
|
||||
}
|
||||
|
||||
db, err := NewDBWithQueue("localhost:2379", config)
|
||||
if err != nil {
|
||||
t.Skipf("etcd not available: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Test basic operations
|
||||
testKey := []byte("test-queue-key")
|
||||
testValue := []byte("test-queue-value")
|
||||
|
||||
err = db.Put(testKey, testValue)
|
||||
if err != nil {
|
||||
t.Fatalf("Put failed: %v", err)
|
||||
}
|
||||
|
||||
// Give queue time to process
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
retrieved, err := db.Get(testKey)
|
||||
if err != nil {
|
||||
t.Fatalf("Get failed: %v", err)
|
||||
}
|
||||
|
||||
if string(retrieved) != string(testValue) {
|
||||
t.Fatalf("Expected %s, got %s", testValue, retrieved)
|
||||
}
|
||||
|
||||
// Clean up
|
||||
err = db.Delete(testKey)
|
||||
if err != nil {
|
||||
t.Fatalf("Delete failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEtcdWithoutQueue(t *testing.T) {
|
||||
// Test with queue disabled
|
||||
config := &QueueConfig{
|
||||
Enabled: false,
|
||||
}
|
||||
|
||||
db, err := NewDBWithQueue("localhost:2379", config)
|
||||
if err != nil {
|
||||
t.Skipf("etcd not available: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Verify it's regular etcd storage
|
||||
_, ok := db.(*EtcDStorage)
|
||||
if !ok {
|
||||
t.Fatal("Expected EtcDStorage type")
|
||||
}
|
||||
|
||||
// Test basic operations
|
||||
testKey := []byte("test-no-queue-key")
|
||||
testValue := []byte("test-no-queue-value")
|
||||
|
||||
err = db.Put(testKey, testValue)
|
||||
if err != nil {
|
||||
t.Fatalf("Put failed: %v", err)
|
||||
}
|
||||
|
||||
retrieved, err := db.Get(testKey)
|
||||
if err != nil {
|
||||
t.Fatalf("Get failed: %v", err)
|
||||
}
|
||||
|
||||
if string(retrieved) != string(testValue) {
|
||||
t.Fatalf("Expected %s, got %s", testValue, retrieved)
|
||||
}
|
||||
|
||||
// Clean up
|
||||
err = db.Delete(testKey)
|
||||
if err != nil {
|
||||
t.Fatalf("Delete failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueueImplementsInterface(t *testing.T) {
|
||||
// Verify that our implementation satisfies the database.Storage interface
|
||||
var _ database.Storage = (*EtcDStorage)(nil)
|
||||
}
|
||||
@@ -1,381 +0,0 @@
|
||||
package etcddb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
clientv3 "go.etcd.io/etcd/client/v3"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// QueueConfig contains configuration for the write queue
|
||||
type QueueConfig struct {
|
||||
Enabled bool
|
||||
WriteQueueSize int
|
||||
MaxWritesPerSec int
|
||||
BatchMaxSize int
|
||||
BatchMaxWait time.Duration
|
||||
}
|
||||
|
||||
// DefaultQueueConfig returns default queue configuration
|
||||
func DefaultQueueConfig() *QueueConfig {
|
||||
return &QueueConfig{
|
||||
Enabled: false,
|
||||
WriteQueueSize: 1000,
|
||||
MaxWritesPerSec: 100,
|
||||
BatchMaxSize: 50,
|
||||
BatchMaxWait: 10 * time.Millisecond,
|
||||
}
|
||||
}
|
||||
|
||||
// writeOp represents a queued write operation
|
||||
type writeOp struct {
|
||||
fn func() error
|
||||
result chan error
|
||||
}
|
||||
|
||||
// QueuedEtcdClient wraps an etcd client with write queueing
|
||||
type QueuedEtcdClient struct {
|
||||
client *clientv3.Client
|
||||
kv clientv3.KV
|
||||
writeQueue chan writeOp
|
||||
config *QueueConfig
|
||||
wg sync.WaitGroup
|
||||
done chan struct{}
|
||||
closed atomic.Bool
|
||||
}
|
||||
|
||||
// NewQueuedEtcdClient creates a new queued etcd client
|
||||
func NewQueuedEtcdClient(client *clientv3.Client, config *QueueConfig) *QueuedEtcdClient {
|
||||
if config == nil {
|
||||
config = DefaultQueueConfig()
|
||||
}
|
||||
|
||||
qc := &QueuedEtcdClient{
|
||||
client: client,
|
||||
kv: client.KV,
|
||||
writeQueue: make(chan writeOp, config.WriteQueueSize),
|
||||
config: config,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
if config.Enabled {
|
||||
qc.wg.Add(1)
|
||||
go qc.processQueue()
|
||||
}
|
||||
|
||||
return qc
|
||||
}
|
||||
|
||||
// processQueue processes write operations sequentially
|
||||
func (qc *QueuedEtcdClient) processQueue() {
|
||||
defer qc.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(time.Second / time.Duration(qc.config.MaxWritesPerSec))
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-qc.done:
|
||||
// Cancel remaining operations
|
||||
for len(qc.writeQueue) > 0 {
|
||||
select {
|
||||
case op := <-qc.writeQueue:
|
||||
op.result <- context.Canceled
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
|
||||
case op := <-qc.writeQueue:
|
||||
if qc.closed.Load() {
|
||||
op.result <- context.Canceled
|
||||
continue
|
||||
}
|
||||
qc.executeOp(op)
|
||||
<-ticker.C // Rate limiting after operation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// executeOp executes a single write operation
|
||||
func (qc *QueuedEtcdClient) executeOp(op writeOp) {
|
||||
start := time.Now()
|
||||
err := op.fn()
|
||||
duration := time.Since(start)
|
||||
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Dur("duration", duration).Msg("etcd write operation failed")
|
||||
} else {
|
||||
log.Debug().Dur("duration", duration).Msg("etcd write operation completed")
|
||||
}
|
||||
|
||||
op.result <- err
|
||||
}
|
||||
|
||||
// Close closes the queued client
|
||||
func (qc *QueuedEtcdClient) Close() error {
|
||||
if qc.config.Enabled {
|
||||
qc.closed.Store(true)
|
||||
close(qc.done)
|
||||
|
||||
// Wait for queue to drain with timeout
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
qc.wg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// Queue drained successfully
|
||||
case <-time.After(5 * time.Second):
|
||||
// Timeout - log warning but continue
|
||||
log.Warn().Msg("etcd: queue close timeout, some operations may be lost")
|
||||
}
|
||||
}
|
||||
return qc.client.Close()
|
||||
}
|
||||
|
||||
// QueuedKV implements clientv3.KV with write queueing
|
||||
type QueuedKV struct {
|
||||
kv clientv3.KV
|
||||
writeQueue chan writeOp
|
||||
config *QueueConfig
|
||||
}
|
||||
|
||||
// NewQueuedKV creates a new queued KV interface
|
||||
func NewQueuedKV(kv clientv3.KV, writeQueue chan writeOp, config *QueueConfig) *QueuedKV {
|
||||
return &QueuedKV{
|
||||
kv: kv,
|
||||
writeQueue: writeQueue,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// Put queues a put operation
|
||||
func (qkv *QueuedKV) Put(ctx context.Context, key, val string, opts ...clientv3.OpOption) (*clientv3.PutResponse, error) {
|
||||
if !qkv.config.Enabled {
|
||||
return qkv.kv.Put(ctx, key, val, opts...)
|
||||
}
|
||||
|
||||
resultChan := make(chan error, 1)
|
||||
respChan := make(chan *clientv3.PutResponse, 1)
|
||||
|
||||
select {
|
||||
case qkv.writeQueue <- writeOp{
|
||||
fn: func() error {
|
||||
resp, err := qkv.kv.Put(ctx, key, val, opts...)
|
||||
if err == nil {
|
||||
respChan <- resp
|
||||
}
|
||||
return err
|
||||
},
|
||||
result: resultChan,
|
||||
}:
|
||||
// Successfully queued
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
select {
|
||||
case err := <-resultChan:
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return <-respChan, nil
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// Get performs a get operation (not queued)
|
||||
func (qkv *QueuedKV) Get(ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.GetResponse, error) {
|
||||
return qkv.kv.Get(ctx, key, opts...)
|
||||
}
|
||||
|
||||
// Delete queues a delete operation
|
||||
func (qkv *QueuedKV) Delete(ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.DeleteResponse, error) {
|
||||
if !qkv.config.Enabled {
|
||||
return qkv.kv.Delete(ctx, key, opts...)
|
||||
}
|
||||
|
||||
resultChan := make(chan error, 1)
|
||||
respChan := make(chan *clientv3.DeleteResponse, 1)
|
||||
|
||||
select {
|
||||
case qkv.writeQueue <- writeOp{
|
||||
fn: func() error {
|
||||
resp, err := qkv.kv.Delete(ctx, key, opts...)
|
||||
if err == nil {
|
||||
respChan <- resp
|
||||
}
|
||||
return err
|
||||
},
|
||||
result: resultChan,
|
||||
}:
|
||||
// Successfully queued
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
select {
|
||||
case err := <-resultChan:
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return <-respChan, nil
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// Txn creates a transaction (will be queued)
|
||||
func (qkv *QueuedKV) Txn(ctx context.Context) clientv3.Txn {
|
||||
return &QueuedTxn{
|
||||
txn: qkv.kv.Txn(ctx),
|
||||
writeQueue: qkv.writeQueue,
|
||||
config: qkv.config,
|
||||
ctx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
// Do performs a generic operation
|
||||
func (qkv *QueuedKV) Do(ctx context.Context, op clientv3.Op) (clientv3.OpResponse, error) {
|
||||
// Determine if this is a write operation
|
||||
if op.IsGet() {
|
||||
// Read operations are not queued
|
||||
return qkv.kv.Do(ctx, op)
|
||||
}
|
||||
|
||||
if !qkv.config.Enabled {
|
||||
return qkv.kv.Do(ctx, op)
|
||||
}
|
||||
|
||||
// Queue write operations
|
||||
resultChan := make(chan error, 1)
|
||||
respChan := make(chan clientv3.OpResponse, 1)
|
||||
|
||||
select {
|
||||
case qkv.writeQueue <- writeOp{
|
||||
fn: func() error {
|
||||
resp, err := qkv.kv.Do(ctx, op)
|
||||
if err == nil {
|
||||
respChan <- resp
|
||||
}
|
||||
return err
|
||||
},
|
||||
result: resultChan,
|
||||
}:
|
||||
// Successfully queued
|
||||
case <-ctx.Done():
|
||||
return clientv3.OpResponse{}, ctx.Err()
|
||||
}
|
||||
|
||||
err := <-resultChan
|
||||
if err != nil {
|
||||
return clientv3.OpResponse{}, err
|
||||
}
|
||||
return <-respChan, nil
|
||||
}
|
||||
|
||||
// Compact queues a compact operation
|
||||
func (qkv *QueuedKV) Compact(ctx context.Context, rev int64, opts ...clientv3.CompactOption) (*clientv3.CompactResponse, error) {
|
||||
if !qkv.config.Enabled {
|
||||
return qkv.kv.Compact(ctx, rev, opts...)
|
||||
}
|
||||
|
||||
resultChan := make(chan error, 1)
|
||||
respChan := make(chan *clientv3.CompactResponse, 1)
|
||||
|
||||
select {
|
||||
case qkv.writeQueue <- writeOp{
|
||||
fn: func() error {
|
||||
resp, err := qkv.kv.Compact(ctx, rev, opts...)
|
||||
if err == nil {
|
||||
respChan <- resp
|
||||
}
|
||||
return err
|
||||
},
|
||||
result: resultChan,
|
||||
}:
|
||||
// Successfully queued
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
select {
|
||||
case err := <-resultChan:
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return <-respChan, nil
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// QueuedTxn wraps a transaction with queueing
|
||||
type QueuedTxn struct {
|
||||
txn clientv3.Txn
|
||||
writeQueue chan writeOp
|
||||
config *QueueConfig
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// If sets the comparison target
|
||||
func (qtxn *QueuedTxn) If(cs ...clientv3.Cmp) clientv3.Txn {
|
||||
qtxn.txn = qtxn.txn.If(cs...)
|
||||
return qtxn
|
||||
}
|
||||
|
||||
// Then sets the success operations
|
||||
func (qtxn *QueuedTxn) Then(ops ...clientv3.Op) clientv3.Txn {
|
||||
qtxn.txn = qtxn.txn.Then(ops...)
|
||||
return qtxn
|
||||
}
|
||||
|
||||
// Else sets the failure operations
|
||||
func (qtxn *QueuedTxn) Else(ops ...clientv3.Op) clientv3.Txn {
|
||||
qtxn.txn = qtxn.txn.Else(ops...)
|
||||
return qtxn
|
||||
}
|
||||
|
||||
// Commit queues the transaction commit
|
||||
func (qtxn *QueuedTxn) Commit() (*clientv3.TxnResponse, error) {
|
||||
if !qtxn.config.Enabled {
|
||||
return qtxn.txn.Commit()
|
||||
}
|
||||
|
||||
resultChan := make(chan error, 1)
|
||||
respChan := make(chan *clientv3.TxnResponse, 1)
|
||||
|
||||
select {
|
||||
case qtxn.writeQueue <- writeOp{
|
||||
fn: func() error {
|
||||
resp, err := qtxn.txn.Commit()
|
||||
if err == nil {
|
||||
respChan <- resp
|
||||
}
|
||||
return err
|
||||
},
|
||||
result: resultChan,
|
||||
}:
|
||||
// Successfully queued
|
||||
case <-qtxn.ctx.Done():
|
||||
return nil, qtxn.ctx.Err()
|
||||
}
|
||||
|
||||
select {
|
||||
case err := <-resultChan:
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return <-respChan, nil
|
||||
case <-qtxn.ctx.Done():
|
||||
return nil, qtxn.ctx.Err()
|
||||
}
|
||||
}
|
||||
@@ -1,384 +0,0 @@
|
||||
package etcddb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
clientv3 "go.etcd.io/etcd/client/v3"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type QueueSuite struct {
|
||||
client *clientv3.Client
|
||||
config *QueueConfig
|
||||
}
|
||||
|
||||
var _ = Suite(&QueueSuite{})
|
||||
|
||||
func TestQueue(t *testing.T) { TestingT(t) }
|
||||
|
||||
func (s *QueueSuite) SetUpSuite(c *C) {
|
||||
// Create a test etcd client
|
||||
cli, err := clientv3.New(clientv3.Config{
|
||||
Endpoints: []string{"localhost:2379"},
|
||||
DialTimeout: 5 * time.Second,
|
||||
})
|
||||
if err != nil {
|
||||
c.Skip("etcd not available: " + err.Error())
|
||||
}
|
||||
s.client = cli
|
||||
}
|
||||
|
||||
func (s *QueueSuite) TearDownSuite(c *C) {
|
||||
if s.client != nil {
|
||||
s.client.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *QueueSuite) SetUpTest(c *C) {
|
||||
s.config = &QueueConfig{
|
||||
Enabled: true,
|
||||
WriteQueueSize: 100,
|
||||
MaxWritesPerSec: 100, // Faster for tests
|
||||
BatchMaxSize: 10,
|
||||
BatchMaxWait: 10 * time.Millisecond,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *QueueSuite) TearDownTest(c *C) {
|
||||
// Clean up all test data
|
||||
ctx := context.Background()
|
||||
resp, err := s.client.Get(ctx, "/test/", clientv3.WithPrefix())
|
||||
if err == nil && len(resp.Kvs) > 0 {
|
||||
_, _ = s.client.Delete(ctx, "/test/", clientv3.WithPrefix())
|
||||
}
|
||||
}
|
||||
|
||||
func (s *QueueSuite) TestQueuedClientCreation(c *C) {
|
||||
qc := NewQueuedEtcdClient(s.client, s.config)
|
||||
c.Assert(qc, NotNil)
|
||||
c.Assert(qc.client, Equals, s.client)
|
||||
c.Assert(qc.config, DeepEquals, s.config)
|
||||
c.Assert(cap(qc.writeQueue), Equals, 100)
|
||||
|
||||
err := qc.Close()
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *QueueSuite) TestQueuedKVPut(c *C) {
|
||||
qc := NewQueuedEtcdClient(s.client, s.config)
|
||||
defer qc.Close()
|
||||
|
||||
qkv := NewQueuedKV(s.client.KV, qc.writeQueue, s.config)
|
||||
|
||||
ctx := context.Background()
|
||||
key := "/test/queue/put"
|
||||
value := "test-value"
|
||||
|
||||
// Clean up first
|
||||
s.client.Delete(ctx, key)
|
||||
|
||||
// Put via queued KV
|
||||
_, err := qkv.Put(ctx, key, value)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
// Give queue time to process
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// Verify via direct client
|
||||
resp, err := s.client.Get(ctx, key)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(len(resp.Kvs), Equals, 1)
|
||||
c.Assert(string(resp.Kvs[0].Value), Equals, value)
|
||||
|
||||
// Clean up
|
||||
s.client.Delete(ctx, key)
|
||||
}
|
||||
|
||||
func (s *QueueSuite) TestQueuedKVDelete(c *C) {
|
||||
qc := NewQueuedEtcdClient(s.client, s.config)
|
||||
defer qc.Close()
|
||||
|
||||
qkv := NewQueuedKV(s.client.KV, qc.writeQueue, s.config)
|
||||
|
||||
ctx := context.Background()
|
||||
key := "/test/queue/delete"
|
||||
value := "test-value"
|
||||
|
||||
// Put directly
|
||||
_, err := s.client.Put(ctx, key, value)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
// Delete via queued KV
|
||||
_, err = qkv.Delete(ctx, key)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
// Give queue time to process
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// Verify deletion
|
||||
resp, err := s.client.Get(ctx, key)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(len(resp.Kvs), Equals, 0)
|
||||
}
|
||||
|
||||
func (s *QueueSuite) TestQueuedTransaction(c *C) {
|
||||
qc := NewQueuedEtcdClient(s.client, s.config)
|
||||
defer qc.Close()
|
||||
|
||||
qkv := NewQueuedKV(s.client.KV, qc.writeQueue, s.config)
|
||||
|
||||
ctx := context.Background()
|
||||
key1 := "/test/queue/txn1"
|
||||
key2 := "/test/queue/txn2"
|
||||
value1 := "value1"
|
||||
value2 := "value2"
|
||||
|
||||
// Clean up first
|
||||
s.client.Delete(ctx, key1)
|
||||
s.client.Delete(ctx, key2)
|
||||
|
||||
// Create transaction
|
||||
txn := qkv.Txn(ctx)
|
||||
txn = txn.If().Then(
|
||||
clientv3.OpPut(key1, value1),
|
||||
clientv3.OpPut(key2, value2),
|
||||
)
|
||||
|
||||
// Commit via queued transaction
|
||||
_, err := txn.Commit()
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
// Give queue time to process
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// Verify both keys exist
|
||||
resp1, err := s.client.Get(ctx, key1)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(len(resp1.Kvs), Equals, 1)
|
||||
c.Assert(string(resp1.Kvs[0].Value), Equals, value1)
|
||||
|
||||
resp2, err := s.client.Get(ctx, key2)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(len(resp2.Kvs), Equals, 1)
|
||||
c.Assert(string(resp2.Kvs[0].Value), Equals, value2)
|
||||
|
||||
// Clean up
|
||||
s.client.Delete(ctx, key1)
|
||||
s.client.Delete(ctx, key2)
|
||||
}
|
||||
|
||||
func (s *QueueSuite) TestRateLimiting(c *C) {
|
||||
// Configure very low rate limit
|
||||
config := &QueueConfig{
|
||||
Enabled: true,
|
||||
WriteQueueSize: 100,
|
||||
MaxWritesPerSec: 5, // Only 5 writes per second
|
||||
BatchMaxSize: 10,
|
||||
BatchMaxWait: 10 * time.Millisecond,
|
||||
}
|
||||
|
||||
qc := NewQueuedEtcdClient(s.client, config)
|
||||
defer qc.Close()
|
||||
|
||||
qkv := NewQueuedKV(s.client.KV, qc.writeQueue, config)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Time 10 operations
|
||||
start := time.Now()
|
||||
keys := make([]string, 10)
|
||||
for i := 0; i < 10; i++ {
|
||||
key := fmt.Sprintf("/test/queue/rate/%d", i)
|
||||
keys[i] = key
|
||||
_, err := qkv.Put(ctx, key, "value")
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
// Clean up after test
|
||||
defer func() {
|
||||
for _, key := range keys {
|
||||
s.client.Delete(ctx, key)
|
||||
}
|
||||
}()
|
||||
|
||||
// Give queue time to process all
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
// With rate limit of 5/sec, 10 operations should take at least 2 seconds
|
||||
elapsed := time.Since(start)
|
||||
c.Assert(elapsed >= 2*time.Second, Equals, true, Commentf("Operations completed too fast: %v", elapsed))
|
||||
}
|
||||
|
||||
func (s *QueueSuite) TestConcurrentWrites(c *C) {
|
||||
qc := NewQueuedEtcdClient(s.client, s.config)
|
||||
defer qc.Close()
|
||||
|
||||
qkv := NewQueuedKV(s.client.KV, qc.writeQueue, s.config)
|
||||
|
||||
ctx := context.Background()
|
||||
var wg sync.WaitGroup
|
||||
numWriters := 20
|
||||
writesPerWriter := 5
|
||||
|
||||
var successCount int32
|
||||
|
||||
// Launch concurrent writers
|
||||
for i := 0; i < numWriters; i++ {
|
||||
wg.Add(1)
|
||||
go func(writerID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := 0; j < writesPerWriter; j++ {
|
||||
key := fmt.Sprintf("/test/queue/concurrent/%d/%d", writerID, j)
|
||||
value := "value"
|
||||
|
||||
_, err := qkv.Put(ctx, key, value)
|
||||
if err == nil {
|
||||
atomic.AddInt32(&successCount, 1)
|
||||
} else {
|
||||
c.Logf("Write failed: %v", err)
|
||||
}
|
||||
|
||||
// Clean up immediately
|
||||
if err == nil {
|
||||
s.client.Delete(ctx, key)
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Wait for all writers
|
||||
wg.Wait()
|
||||
|
||||
// Give queue time to process remaining
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// All writes should succeed
|
||||
c.Assert(int(successCount), Equals, numWriters*writesPerWriter)
|
||||
}
|
||||
|
||||
func (s *QueueSuite) TestQueueOverflow(c *C) {
|
||||
c.Skip("Test has blocking issues when queue is full")
|
||||
// This test verifies that when the queue is full, operations don't block indefinitely
|
||||
// Instead, with a small queue, we expect the queue to process items quickly
|
||||
config := &QueueConfig{
|
||||
Enabled: true,
|
||||
WriteQueueSize: 10, // Small queue but not too small
|
||||
MaxWritesPerSec: 100, // Fast processing
|
||||
BatchMaxSize: 10,
|
||||
BatchMaxWait: 10 * time.Millisecond,
|
||||
}
|
||||
|
||||
qc := NewQueuedEtcdClient(s.client, config)
|
||||
defer qc.Close()
|
||||
|
||||
qkv := NewQueuedKV(s.client.KV, qc.writeQueue, config)
|
||||
|
||||
ctx := context.Background()
|
||||
var wg sync.WaitGroup
|
||||
errors := make(chan error, 20)
|
||||
|
||||
// Launch 20 concurrent writers
|
||||
for i := 0; i < 20; i++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
key := fmt.Sprintf("/test/queue/overflow/%d", idx)
|
||||
_, err := qkv.Put(ctx, key, "value")
|
||||
if err != nil {
|
||||
errors <- err
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Wait for all writers to complete
|
||||
wg.Wait()
|
||||
close(errors)
|
||||
|
||||
// Check for errors
|
||||
for err := range errors {
|
||||
c.Fatalf("Queue operation failed: %v", err)
|
||||
}
|
||||
|
||||
// Give queue time to finish processing
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
func (s *QueueSuite) TestDisabledQueue(c *C) {
|
||||
// Create disabled queue
|
||||
config := &QueueConfig{
|
||||
Enabled: false,
|
||||
}
|
||||
|
||||
qc := NewQueuedEtcdClient(s.client, config)
|
||||
defer qc.Close()
|
||||
|
||||
qkv := NewQueuedKV(s.client.KV, qc.writeQueue, config)
|
||||
|
||||
ctx := context.Background()
|
||||
key := "/test/queue/disabled"
|
||||
value := "test-value"
|
||||
|
||||
// Clean up first
|
||||
s.client.Delete(ctx, key)
|
||||
|
||||
// Put should go directly to etcd
|
||||
start := time.Now()
|
||||
_, err := qkv.Put(ctx, key, value)
|
||||
c.Assert(err, IsNil)
|
||||
elapsed := time.Since(start)
|
||||
|
||||
// Should be fast (no queueing)
|
||||
c.Assert(elapsed < 100*time.Millisecond, Equals, true)
|
||||
|
||||
// Verify immediately
|
||||
resp, err := s.client.Get(ctx, key)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(len(resp.Kvs), Equals, 1)
|
||||
c.Assert(string(resp.Kvs[0].Value), Equals, value)
|
||||
|
||||
// Clean up
|
||||
s.client.Delete(ctx, key)
|
||||
}
|
||||
|
||||
// TestIntegrationWithStorage tests the queue with actual EtcDStorage
|
||||
func (s *QueueSuite) TestIntegrationWithStorage(c *C) {
|
||||
// Create storage with queue
|
||||
storage, err := NewDBWithQueue("localhost:2379", s.config)
|
||||
c.Assert(err, IsNil)
|
||||
defer storage.Close()
|
||||
|
||||
etcdStorage := storage.(*EtcDStorage)
|
||||
c.Assert(etcdStorage.queuedClient, NotNil)
|
||||
c.Assert(etcdStorage.queuedKV, NotNil)
|
||||
|
||||
// Test Put/Get operations
|
||||
key := []byte("test-integration-key")
|
||||
value := []byte("test-integration-value")
|
||||
|
||||
err = etcdStorage.Put(key, value)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
// Give queue time to process
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
retrieved, err := etcdStorage.Get(key)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(retrieved, DeepEquals, value)
|
||||
|
||||
// Test Delete
|
||||
err = etcdStorage.Delete(key)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
// Give queue time to process
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
retrieved, err = etcdStorage.Get(key)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(retrieved, IsNil)
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package etcddb
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestQueueConfigDefaults(t *testing.T) {
|
||||
config := &QueueConfig{
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
// Test default values
|
||||
if config.WriteQueueSize == 0 {
|
||||
config.WriteQueueSize = 1000
|
||||
}
|
||||
if config.MaxWritesPerSec == 0 {
|
||||
config.MaxWritesPerSec = 100
|
||||
}
|
||||
if config.BatchMaxSize == 0 {
|
||||
config.BatchMaxSize = 50
|
||||
}
|
||||
if config.BatchMaxWait == 0 {
|
||||
config.BatchMaxWait = 10 * time.Millisecond
|
||||
}
|
||||
|
||||
// Verify defaults
|
||||
if config.WriteQueueSize != 1000 {
|
||||
t.Errorf("Expected default WriteQueueSize to be 1000, got %d", config.WriteQueueSize)
|
||||
}
|
||||
if config.MaxWritesPerSec != 100 {
|
||||
t.Errorf("Expected default MaxWritesPerSec to be 100, got %d", config.MaxWritesPerSec)
|
||||
}
|
||||
if config.BatchMaxSize != 50 {
|
||||
t.Errorf("Expected default BatchMaxSize to be 50, got %d", config.BatchMaxSize)
|
||||
}
|
||||
if config.BatchMaxWait != 10*time.Millisecond {
|
||||
t.Errorf("Expected default BatchMaxWait to be 10ms, got %v", config.BatchMaxWait)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueueConfigDisabled(t *testing.T) {
|
||||
config := &QueueConfig{
|
||||
Enabled: false,
|
||||
}
|
||||
|
||||
if config.Enabled {
|
||||
t.Error("Expected queue to be disabled")
|
||||
}
|
||||
}
|
||||
|
||||
+17
-141
@@ -1,34 +1,26 @@
|
||||
package etcddb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aptly-dev/aptly/database"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
clientv3 "go.etcd.io/etcd/client/v3"
|
||||
)
|
||||
|
||||
type EtcDStorage struct {
|
||||
url string
|
||||
db *clientv3.Client
|
||||
queuedClient *QueuedEtcdClient
|
||||
queuedKV *QueuedKV
|
||||
tmpPrefix string // prefix for temporary DBs
|
||||
url string
|
||||
db *clientv3.Client
|
||||
tmpPrefix string // prefix for temporary DBs
|
||||
}
|
||||
|
||||
// CreateTemporary creates new DB of the same type in temp dir
|
||||
func (s *EtcDStorage) CreateTemporary() (database.Storage, error) {
|
||||
tmp := uuid.NewString()
|
||||
return &EtcDStorage{
|
||||
url: s.url,
|
||||
db: s.db,
|
||||
queuedClient: s.queuedClient,
|
||||
queuedKV: s.queuedKV,
|
||||
tmpPrefix: tmp,
|
||||
url: s.url,
|
||||
db: s.db,
|
||||
tmpPrefix: tmp,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -39,70 +31,11 @@ func (s *EtcDStorage) applyPrefix(key []byte) []byte {
|
||||
return key
|
||||
}
|
||||
|
||||
// getContext returns a context with timeout for etcd operations
|
||||
func (s *EtcDStorage) getContext() (context.Context, context.CancelFunc) {
|
||||
return context.WithTimeout(context.Background(), DefaultTimeout)
|
||||
}
|
||||
|
||||
// isTemporary checks if error is temporary and can be retried
|
||||
func isTemporary(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for context deadline exceeded
|
||||
if err == context.DeadlineExceeded {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for etcd specific temporary errors
|
||||
switch err {
|
||||
case clientv3.ErrNoAvailableEndpoints:
|
||||
return true
|
||||
default:
|
||||
// Check if error string contains temporary indicators
|
||||
errStr := err.Error()
|
||||
return strings.Contains(errStr, "temporary") ||
|
||||
strings.Contains(errStr, "timeout") ||
|
||||
strings.Contains(errStr, "unavailable") ||
|
||||
strings.Contains(errStr, "connection refused")
|
||||
}
|
||||
}
|
||||
|
||||
// Get key value from etcd
|
||||
func (s *EtcDStorage) Get(key []byte) (value []byte, err error) {
|
||||
realKey := s.applyPrefix(key)
|
||||
|
||||
var getResp *clientv3.GetResponse
|
||||
maxRetries := 3
|
||||
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
ctx, cancel := s.getContext()
|
||||
if s.queuedKV != nil {
|
||||
getResp, err = s.queuedKV.Get(ctx, string(realKey))
|
||||
} else {
|
||||
getResp, err = s.db.Get(ctx, string(realKey))
|
||||
}
|
||||
cancel()
|
||||
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
// Only retry on temporary errors and not on last attempt
|
||||
if i < maxRetries-1 && isTemporary(err) {
|
||||
backoff := time.Duration(i+1) * 100 * time.Millisecond
|
||||
log.Warn().
|
||||
Err(err).
|
||||
Str("key", string(realKey)).
|
||||
Int("attempt", i+1).
|
||||
Dur("backoff", backoff).
|
||||
Msg("etcd: get failed, retrying")
|
||||
time.Sleep(backoff)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Error().Err(err).Str("key", string(realKey)).Msg("etcd: get failed")
|
||||
getResp, err := s.db.Get(Ctx, string(realKey))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, kv := range getResp.Kvs {
|
||||
@@ -119,17 +52,8 @@ func (s *EtcDStorage) Get(key []byte) (value []byte, err error) {
|
||||
// Put saves key to etcd, if key has the same value in DB already, it is not saved
|
||||
func (s *EtcDStorage) Put(key []byte, value []byte) (err error) {
|
||||
realKey := s.applyPrefix(key)
|
||||
|
||||
ctx, cancel := s.getContext()
|
||||
defer cancel()
|
||||
|
||||
if s.queuedKV != nil {
|
||||
_, err = s.queuedKV.Put(ctx, string(realKey), string(value))
|
||||
} else {
|
||||
_, err = s.db.Put(ctx, string(realKey), string(value))
|
||||
}
|
||||
_, err = s.db.Put(Ctx, string(realKey), string(value))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("key", string(realKey)).Msg("etcd: put failed")
|
||||
return
|
||||
}
|
||||
return
|
||||
@@ -138,17 +62,8 @@ func (s *EtcDStorage) Put(key []byte, value []byte) (err error) {
|
||||
// Delete removes key from etcd
|
||||
func (s *EtcDStorage) Delete(key []byte) (err error) {
|
||||
realKey := s.applyPrefix(key)
|
||||
|
||||
ctx, cancel := s.getContext()
|
||||
defer cancel()
|
||||
|
||||
if s.queuedKV != nil {
|
||||
_, err = s.queuedKV.Delete(ctx, string(realKey))
|
||||
} else {
|
||||
_, err = s.db.Delete(ctx, string(realKey))
|
||||
}
|
||||
_, err = s.db.Delete(Ctx, string(realKey))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("key", string(realKey)).Msg("etcd: delete failed")
|
||||
return
|
||||
}
|
||||
return
|
||||
@@ -158,19 +73,8 @@ func (s *EtcDStorage) Delete(key []byte) (err error) {
|
||||
func (s *EtcDStorage) KeysByPrefix(prefix []byte) [][]byte {
|
||||
realPrefix := s.applyPrefix(prefix)
|
||||
result := make([][]byte, 0, 20)
|
||||
|
||||
ctx, cancel := s.getContext()
|
||||
defer cancel()
|
||||
|
||||
var getResp *clientv3.GetResponse
|
||||
var err error
|
||||
if s.queuedKV != nil {
|
||||
getResp, err = s.queuedKV.Get(ctx, string(realPrefix), clientv3.WithPrefix())
|
||||
} else {
|
||||
getResp, err = s.db.Get(ctx, string(realPrefix), clientv3.WithPrefix())
|
||||
}
|
||||
getResp, err := s.db.Get(Ctx, string(realPrefix), clientv3.WithPrefix())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("prefix", string(realPrefix)).Msg("etcd: keys by prefix failed")
|
||||
return nil
|
||||
}
|
||||
for _, ev := range getResp.Kvs {
|
||||
@@ -186,13 +90,8 @@ func (s *EtcDStorage) KeysByPrefix(prefix []byte) [][]byte {
|
||||
func (s *EtcDStorage) FetchByPrefix(prefix []byte) [][]byte {
|
||||
realPrefix := s.applyPrefix(prefix)
|
||||
result := make([][]byte, 0, 20)
|
||||
|
||||
ctx, cancel := s.getContext()
|
||||
defer cancel()
|
||||
|
||||
getResp, err := s.db.Get(ctx, string(realPrefix), clientv3.WithPrefix())
|
||||
getResp, err := s.db.Get(Ctx, string(realPrefix), clientv3.WithPrefix())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("prefix", string(realPrefix)).Msg("etcd: fetch by prefix failed")
|
||||
return nil
|
||||
}
|
||||
for _, kv := range getResp.Kvs {
|
||||
@@ -207,13 +106,8 @@ func (s *EtcDStorage) FetchByPrefix(prefix []byte) [][]byte {
|
||||
// HasPrefix checks whether it can find any key with given prefix and returns true if one exists
|
||||
func (s *EtcDStorage) HasPrefix(prefix []byte) bool {
|
||||
realPrefix := s.applyPrefix(prefix)
|
||||
|
||||
ctx, cancel := s.getContext()
|
||||
defer cancel()
|
||||
|
||||
getResp, err := s.db.Get(ctx, string(realPrefix), clientv3.WithPrefix())
|
||||
getResp, err := s.db.Get(Ctx, string(realPrefix), clientv3.WithPrefix())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("prefix", string(realPrefix)).Msg("etcd: has prefix failed")
|
||||
return false
|
||||
}
|
||||
return getResp.Count > 0
|
||||
@@ -223,13 +117,8 @@ func (s *EtcDStorage) HasPrefix(prefix []byte) bool {
|
||||
// StorageProcessor on key value pair
|
||||
func (s *EtcDStorage) ProcessByPrefix(prefix []byte, proc database.StorageProcessor) error {
|
||||
realPrefix := s.applyPrefix(prefix)
|
||||
|
||||
ctx, cancel := s.getContext()
|
||||
defer cancel()
|
||||
|
||||
getResp, err := s.db.Get(ctx, string(realPrefix), clientv3.WithPrefix())
|
||||
getResp, err := s.db.Get(Ctx, string(realPrefix), clientv3.WithPrefix())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("prefix", string(realPrefix)).Msg("etcd: process by prefix failed")
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -248,16 +137,6 @@ func (s *EtcDStorage) Close() error {
|
||||
if len(s.tmpPrefix) != 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if s.queuedClient != nil {
|
||||
// Close queued client first
|
||||
if err := s.queuedClient.Close(); err != nil {
|
||||
log.Warn().Err(err).Msg("etcd: error closing queued client")
|
||||
}
|
||||
s.queuedClient = nil
|
||||
s.queuedKV = nil
|
||||
}
|
||||
|
||||
if s.db == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -266,7 +145,7 @@ func (s *EtcDStorage) Close() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Open returns the database
|
||||
// Reopen tries to open (re-open) the database
|
||||
func (s *EtcDStorage) Open() error {
|
||||
if s.db != nil {
|
||||
return nil
|
||||
@@ -303,15 +182,12 @@ func (s *EtcDStorage) CompactDB() error {
|
||||
// Drop removes only temporary DBs with etcd (i.e. remove all prefixed keys)
|
||||
func (s *EtcDStorage) Drop() error {
|
||||
if len(s.tmpPrefix) != 0 {
|
||||
ctx, cancel := s.getContext()
|
||||
defer cancel()
|
||||
|
||||
getResp, err := s.db.Get(ctx, s.tmpPrefix, clientv3.WithPrefix())
|
||||
getResp, err := s.db.Get(Ctx, s.tmpPrefix, clientv3.WithPrefix())
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
for _, kv := range getResp.Kvs {
|
||||
_, err = s.db.Delete(ctx, string(kv.Key))
|
||||
_, err = s.db.Delete(Ctx, string(kv.Key))
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot delete tempdb entry: %s", kv.Key)
|
||||
}
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
package etcddb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type StorageSuite struct{}
|
||||
|
||||
var _ = Suite(&StorageSuite{})
|
||||
|
||||
func Test(t *testing.T) { TestingT(t) }
|
||||
|
||||
func (s *StorageSuite) TestGetContext(c *C) {
|
||||
storage := &EtcDStorage{}
|
||||
|
||||
// Test default timeout
|
||||
ctx, cancel := storage.getContext()
|
||||
defer cancel()
|
||||
|
||||
deadline, ok := ctx.Deadline()
|
||||
c.Assert(ok, Equals, true)
|
||||
|
||||
// Should have a deadline set
|
||||
remaining := time.Until(deadline)
|
||||
c.Assert(remaining > 0, Equals, true)
|
||||
c.Assert(remaining <= DefaultTimeout, Equals, true)
|
||||
}
|
||||
|
||||
func (s *StorageSuite) TestDefaultTimeout(c *C) {
|
||||
// Default should be 60 seconds
|
||||
c.Assert(DefaultTimeout, Equals, 60*time.Second)
|
||||
}
|
||||
|
||||
func (s *StorageSuite) TestEnvironmentVariables(c *C) {
|
||||
// Save original values
|
||||
originalTimeout := os.Getenv("APTLY_ETCD_TIMEOUT")
|
||||
originalDialTimeout := os.Getenv("APTLY_ETCD_DIAL_TIMEOUT")
|
||||
originalKeepAlive := os.Getenv("APTLY_ETCD_KEEPALIVE")
|
||||
originalMaxMsg := os.Getenv("APTLY_ETCD_MAX_MSG_SIZE")
|
||||
|
||||
defer func() {
|
||||
// Restore original values
|
||||
os.Setenv("APTLY_ETCD_TIMEOUT", originalTimeout)
|
||||
os.Setenv("APTLY_ETCD_DIAL_TIMEOUT", originalDialTimeout)
|
||||
os.Setenv("APTLY_ETCD_KEEPALIVE", originalKeepAlive)
|
||||
os.Setenv("APTLY_ETCD_MAX_MSG_SIZE", originalMaxMsg)
|
||||
}()
|
||||
|
||||
// Test valid timeout
|
||||
os.Setenv("APTLY_ETCD_TIMEOUT", "30s")
|
||||
// Would need to reinitialize to test, but we can't easily do that
|
||||
// This test mainly ensures the env vars are recognized
|
||||
|
||||
// Test invalid timeout (should use default)
|
||||
os.Setenv("APTLY_ETCD_TIMEOUT", "invalid")
|
||||
timeout := os.Getenv("APTLY_ETCD_TIMEOUT")
|
||||
c.Assert(timeout, Equals, "invalid")
|
||||
}
|
||||
|
||||
func (s *StorageSuite) TestIsTemporary(c *C) {
|
||||
// Test nil error
|
||||
c.Assert(isTemporary(nil), Equals, false)
|
||||
|
||||
// Test context deadline exceeded
|
||||
c.Assert(isTemporary(context.DeadlineExceeded), Equals, true)
|
||||
|
||||
// Test timeout error
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond)
|
||||
defer cancel()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
<-ctx.Done()
|
||||
c.Assert(isTemporary(ctx.Err()), Equals, true)
|
||||
}
|
||||
|
||||
func (s *StorageSuite) TestApplyPrefix(c *C) {
|
||||
// Test without temp prefix
|
||||
storage := &EtcDStorage{}
|
||||
key := []byte("test-key")
|
||||
result := storage.applyPrefix(key)
|
||||
c.Assert(result, DeepEquals, key)
|
||||
|
||||
// Test with temp prefix
|
||||
storage.tmpPrefix = "temp123"
|
||||
result = storage.applyPrefix(key)
|
||||
expected := append([]byte("temp123/"), key...)
|
||||
c.Assert(result, DeepEquals, expected)
|
||||
}
|
||||
|
||||
// Mock test for retry logic
|
||||
func (s *StorageSuite) TestGetRetryLogic(c *C) {
|
||||
// This would require mocking etcd client, which is complex
|
||||
// The test verifies the retry logic exists and compiles
|
||||
// In production, this would be tested with integration tests
|
||||
|
||||
// Verify retry count
|
||||
maxRetries := 3
|
||||
c.Assert(maxRetries, Equals, 3)
|
||||
|
||||
// Verify backoff calculation
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
backoff := time.Duration(i+1) * 100 * time.Millisecond
|
||||
c.Assert(backoff >= 100*time.Millisecond, Equals, true)
|
||||
c.Assert(backoff <= 300*time.Millisecond, Equals, true)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
package etcddb
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/aptly-dev/aptly/database"
|
||||
clientv3 "go.etcd.io/etcd/client/v3"
|
||||
)
|
||||
@@ -48,9 +46,7 @@ func (t *transaction) Commit() (err error) {
|
||||
|
||||
batchSize := 128
|
||||
for i := 0; i < len(t.ops); i += batchSize {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), DefaultTimeout)
|
||||
defer cancel()
|
||||
txn := kv.Txn(ctx)
|
||||
txn := kv.Txn(Ctx)
|
||||
end := i + batchSize
|
||||
if end > len(t.ops) {
|
||||
end = len(t.ops)
|
||||
@@ -71,7 +67,8 @@ func (t *transaction) Commit() (err error) {
|
||||
// Discard is safe to call after Commit(), it would be no-op
|
||||
func (t *transaction) Discard() {
|
||||
t.ops = []clientv3.Op{}
|
||||
_ = t.tmpdb.Drop()
|
||||
t.tmpdb.Drop()
|
||||
return
|
||||
}
|
||||
|
||||
// transaction should implement database.Transaction
|
||||
|
||||
@@ -51,8 +51,8 @@ func RecoverDB(path string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = db.Close()
|
||||
_ = stor.Close()
|
||||
db.Close()
|
||||
stor.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,519 +0,0 @@
|
||||
package goleveldb_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
|
||||
"github.com/aptly-dev/aptly/database"
|
||||
"github.com/aptly-dev/aptly/database/goleveldb"
|
||||
)
|
||||
|
||||
type ExtendedLevelDBSuite struct {
|
||||
tempDir string
|
||||
}
|
||||
|
||||
var _ = Suite(&ExtendedLevelDBSuite{})
|
||||
|
||||
func (s *ExtendedLevelDBSuite) SetUpTest(c *C) {
|
||||
s.tempDir = c.MkDir()
|
||||
}
|
||||
|
||||
func (s *ExtendedLevelDBSuite) TestNewDB(c *C) {
|
||||
// Test NewDB function
|
||||
dbPath := filepath.Join(s.tempDir, "test-db")
|
||||
|
||||
db, err := goleveldb.NewDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(db, NotNil)
|
||||
|
||||
// DB should not be open yet
|
||||
_, err = db.Get([]byte("test"))
|
||||
c.Check(err, NotNil) // Should error because DB is not open
|
||||
|
||||
// Open the database
|
||||
err = db.Open()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Now should work
|
||||
_, err = db.Get([]byte("test"))
|
||||
c.Check(err, Equals, database.ErrNotFound) // Key not found but no open error
|
||||
|
||||
err = db.Close()
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *ExtendedLevelDBSuite) TestNewOpenDB(c *C) {
|
||||
// Test NewOpenDB function
|
||||
dbPath := filepath.Join(s.tempDir, "test-open-db")
|
||||
|
||||
db, err := goleveldb.NewOpenDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(db, NotNil)
|
||||
|
||||
// DB should be open and ready to use
|
||||
_, err = db.Get([]byte("test"))
|
||||
c.Check(err, Equals, database.ErrNotFound) // Key not found but no open error
|
||||
|
||||
err = db.Close()
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *ExtendedLevelDBSuite) TestRecoverDBError(c *C) {
|
||||
// Test RecoverDB with invalid path
|
||||
invalidPath := "/invalid/nonexistent/path"
|
||||
|
||||
err := goleveldb.RecoverDB(invalidPath)
|
||||
c.Check(err, NotNil) // Should error with invalid path
|
||||
}
|
||||
|
||||
func (s *ExtendedLevelDBSuite) TestRecoverDBValidPath(c *C) {
|
||||
// Test RecoverDB with valid database
|
||||
dbPath := filepath.Join(s.tempDir, "recover-test")
|
||||
|
||||
// First create a database
|
||||
db, err := goleveldb.NewOpenDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Add some data
|
||||
err = db.Put([]byte("key1"), []byte("value1"))
|
||||
c.Check(err, IsNil)
|
||||
|
||||
err = db.Close()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Now recover it
|
||||
err = goleveldb.RecoverDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Verify data is still there after recovery
|
||||
db2, err := goleveldb.NewOpenDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
value, err := db2.Get([]byte("key1"))
|
||||
c.Check(err, IsNil)
|
||||
c.Check(value, DeepEquals, []byte("value1"))
|
||||
|
||||
err = db2.Close()
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *ExtendedLevelDBSuite) TestCreateTemporaryError(c *C) {
|
||||
// Test CreateTemporary with limited permissions (if possible)
|
||||
dbPath := filepath.Join(s.tempDir, "test-temp")
|
||||
|
||||
db, err := goleveldb.NewOpenDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
tempDB, err := db.CreateTemporary()
|
||||
c.Check(err, IsNil)
|
||||
c.Check(tempDB, NotNil)
|
||||
|
||||
// Temporary DB should be usable
|
||||
err = tempDB.Put([]byte("temp-key"), []byte("temp-value"))
|
||||
c.Check(err, IsNil)
|
||||
|
||||
value, err := tempDB.Get([]byte("temp-key"))
|
||||
c.Check(err, IsNil)
|
||||
c.Check(value, DeepEquals, []byte("temp-value"))
|
||||
|
||||
err = tempDB.Close()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
err = tempDB.Drop()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
err = db.Close()
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *ExtendedLevelDBSuite) TestStoragePutOptimization(c *C) {
|
||||
// Test Put optimization (doesn't save if value is same)
|
||||
dbPath := filepath.Join(s.tempDir, "put-optimization")
|
||||
|
||||
db, err := goleveldb.NewOpenDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
key := []byte("optimization-key")
|
||||
value := []byte("same-value")
|
||||
|
||||
// First put
|
||||
err = db.Put(key, value)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Second put with same value (should be optimized)
|
||||
err = db.Put(key, value)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Third put with different value
|
||||
newValue := []byte("different-value")
|
||||
err = db.Put(key, newValue)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Verify final value
|
||||
result, err := db.Get(key)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(result, DeepEquals, newValue)
|
||||
|
||||
err = db.Close()
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *ExtendedLevelDBSuite) TestStorageCloseMultiple(c *C) {
|
||||
// Test calling Close multiple times
|
||||
dbPath := filepath.Join(s.tempDir, "close-multiple")
|
||||
|
||||
db, err := goleveldb.NewOpenDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// First close should work
|
||||
err = db.Close()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Second close should not error
|
||||
err = db.Close()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Third close should not error
|
||||
err = db.Close()
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *ExtendedLevelDBSuite) TestStorageOpenMultiple(c *C) {
|
||||
// Test calling Open multiple times
|
||||
dbPath := filepath.Join(s.tempDir, "open-multiple")
|
||||
|
||||
db, err := goleveldb.NewDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// First open should work
|
||||
err = db.Open()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Second open should not error (already open)
|
||||
err = db.Open()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Should still be functional
|
||||
err = db.Put([]byte("test"), []byte("value"))
|
||||
c.Check(err, IsNil)
|
||||
|
||||
err = db.Close()
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *ExtendedLevelDBSuite) TestStorageDropError(c *C) {
|
||||
// Test Drop when database is still open
|
||||
dbPath := filepath.Join(s.tempDir, "drop-error")
|
||||
|
||||
db, err := goleveldb.NewOpenDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Try to drop while DB is open (should error)
|
||||
err = db.Drop()
|
||||
c.Check(err, NotNil)
|
||||
c.Check(err.Error(), Equals, "DB is still open")
|
||||
|
||||
// Close and then drop should work
|
||||
err = db.Close()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
err = db.Drop()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Verify directory is gone
|
||||
_, err = os.Stat(dbPath)
|
||||
c.Check(os.IsNotExist(err), Equals, true)
|
||||
}
|
||||
|
||||
func (s *ExtendedLevelDBSuite) TestTransactionInterface(c *C) {
|
||||
// Test transaction functionality
|
||||
dbPath := filepath.Join(s.tempDir, "transaction-test")
|
||||
|
||||
db, err := goleveldb.NewOpenDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Create transaction
|
||||
tx, err := db.OpenTransaction()
|
||||
c.Check(err, IsNil)
|
||||
c.Check(tx, NotNil)
|
||||
|
||||
// Test transaction operations
|
||||
key := []byte("tx-key")
|
||||
value := []byte("tx-value")
|
||||
|
||||
err = tx.Put(key, value)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Value should not be visible outside transaction yet
|
||||
_, err = db.Get(key)
|
||||
c.Check(err, Equals, database.ErrNotFound)
|
||||
|
||||
// But should be visible within transaction
|
||||
txValue, err := tx.Get(key)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(txValue, DeepEquals, value)
|
||||
|
||||
// Commit transaction
|
||||
err = tx.Commit()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Now value should be visible
|
||||
finalValue, err := db.Get(key)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(finalValue, DeepEquals, value)
|
||||
|
||||
err = db.Close()
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *ExtendedLevelDBSuite) TestTransactionDiscard(c *C) {
|
||||
// Test transaction discard functionality
|
||||
dbPath := filepath.Join(s.tempDir, "transaction-discard")
|
||||
|
||||
db, err := goleveldb.NewOpenDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Create transaction
|
||||
tx, err := db.OpenTransaction()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
key := []byte("discard-key")
|
||||
value := []byte("discard-value")
|
||||
|
||||
err = tx.Put(key, value)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Discard transaction
|
||||
tx.Discard()
|
||||
|
||||
// Value should not be visible
|
||||
_, err = db.Get(key)
|
||||
c.Check(err, Equals, database.ErrNotFound)
|
||||
|
||||
err = db.Close()
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *ExtendedLevelDBSuite) TestProcessByPrefixError(c *C) {
|
||||
// Test ProcessByPrefix with processor that returns error
|
||||
dbPath := filepath.Join(s.tempDir, "process-error")
|
||||
|
||||
db, err := goleveldb.NewOpenDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Add some data
|
||||
prefix := []byte("error-")
|
||||
err = db.Put(append(prefix, []byte("key1")...), []byte("value1"))
|
||||
c.Check(err, IsNil)
|
||||
err = db.Put(append(prefix, []byte("key2")...), []byte("value2"))
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Process with error-returning function
|
||||
testError := errors.New("processing error")
|
||||
processedCount := 0
|
||||
|
||||
err = db.ProcessByPrefix(prefix, func(key, value []byte) error {
|
||||
processedCount++
|
||||
if processedCount == 1 {
|
||||
return testError
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
c.Check(err, Equals, testError)
|
||||
c.Check(processedCount, Equals, 1) // Should stop at first error
|
||||
|
||||
err = db.Close()
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *ExtendedLevelDBSuite) TestPrefixOperationsEmptyDB(c *C) {
|
||||
// Test prefix operations on empty database
|
||||
dbPath := filepath.Join(s.tempDir, "empty-prefix")
|
||||
|
||||
db, err := goleveldb.NewOpenDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
prefix := []byte("empty")
|
||||
|
||||
// All prefix operations should return empty results
|
||||
c.Check(db.HasPrefix(prefix), Equals, false)
|
||||
c.Check(db.KeysByPrefix(prefix), DeepEquals, [][]byte{})
|
||||
c.Check(db.FetchByPrefix(prefix), DeepEquals, [][]byte{})
|
||||
|
||||
processedCount := 0
|
||||
err = db.ProcessByPrefix(prefix, func(key, value []byte) error {
|
||||
processedCount++
|
||||
return nil
|
||||
})
|
||||
c.Check(err, IsNil)
|
||||
c.Check(processedCount, Equals, 0)
|
||||
|
||||
err = db.Close()
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *ExtendedLevelDBSuite) TestBatchOperations(c *C) {
|
||||
// Test batch operations in detail
|
||||
dbPath := filepath.Join(s.tempDir, "batch-ops")
|
||||
|
||||
db, err := goleveldb.NewOpenDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Create batch
|
||||
batch := db.CreateBatch()
|
||||
c.Check(batch, NotNil)
|
||||
|
||||
// Add multiple operations to batch
|
||||
keys := [][]byte{
|
||||
[]byte("batch-key-1"),
|
||||
[]byte("batch-key-2"),
|
||||
[]byte("batch-key-3"),
|
||||
}
|
||||
values := [][]byte{
|
||||
[]byte("batch-value-1"),
|
||||
[]byte("batch-value-2"),
|
||||
[]byte("batch-value-3"),
|
||||
}
|
||||
|
||||
for i, key := range keys {
|
||||
err = batch.Put(key, values[i])
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
// Values should not be visible before Write
|
||||
for _, key := range keys {
|
||||
_, err = db.Get(key)
|
||||
c.Check(err, Equals, database.ErrNotFound)
|
||||
}
|
||||
|
||||
// Write batch
|
||||
err = batch.Write()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Now all values should be visible
|
||||
for i, key := range keys {
|
||||
value, err := db.Get(key)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(value, DeepEquals, values[i])
|
||||
}
|
||||
|
||||
err = db.Close()
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *ExtendedLevelDBSuite) TestIteratorEdgeCases(c *C) {
|
||||
// Test iterator edge cases in prefix operations
|
||||
dbPath := filepath.Join(s.tempDir, "iterator-edge")
|
||||
|
||||
db, err := goleveldb.NewOpenDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Add data with similar but different prefixes
|
||||
prefixes := [][]byte{
|
||||
[]byte("test"),
|
||||
[]byte("test-"),
|
||||
[]byte("test-a"),
|
||||
[]byte("test-ab"),
|
||||
[]byte("testing"),
|
||||
[]byte("totally-different"),
|
||||
}
|
||||
|
||||
for i, prefix := range prefixes {
|
||||
key := append(prefix, []byte("key")...)
|
||||
value := []byte{byte(i)}
|
||||
err = db.Put(key, value)
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
// Test exact prefix matching
|
||||
targetPrefix := []byte("test-")
|
||||
keys := db.KeysByPrefix(targetPrefix)
|
||||
values := db.FetchByPrefix(targetPrefix)
|
||||
|
||||
// Should only match keys that start with "test-"
|
||||
expectedCount := 0
|
||||
for _, prefix := range prefixes {
|
||||
testKey := append(prefix, []byte("key")...)
|
||||
if len(testKey) >= len(targetPrefix) {
|
||||
if string(testKey[:len(targetPrefix)]) == string(targetPrefix) {
|
||||
expectedCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.Check(len(keys), Equals, expectedCount)
|
||||
c.Check(len(values), Equals, expectedCount)
|
||||
|
||||
err = db.Close()
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *ExtendedLevelDBSuite) TestCompactDBError(c *C) {
|
||||
// Test CompactDB on closed database
|
||||
dbPath := filepath.Join(s.tempDir, "compact-error")
|
||||
|
||||
db, err := goleveldb.NewOpenDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Close database
|
||||
err = db.Close()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// CompactDB should error on closed database
|
||||
err = db.CompactDB()
|
||||
c.Check(err, NotNil)
|
||||
}
|
||||
|
||||
func (s *ExtendedLevelDBSuite) TestInterface(c *C) {
|
||||
// Test that storage implements database.Storage interface
|
||||
dbPath := filepath.Join(s.tempDir, "interface-test")
|
||||
|
||||
var storage database.Storage
|
||||
storage, err := goleveldb.NewOpenDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(storage, NotNil)
|
||||
|
||||
// Test that all interface methods are available
|
||||
_, err = storage.Get([]byte("test"))
|
||||
c.Check(err, Equals, database.ErrNotFound)
|
||||
|
||||
err = storage.Put([]byte("test"), []byte("value"))
|
||||
c.Check(err, IsNil)
|
||||
|
||||
err = storage.Delete([]byte("test"))
|
||||
c.Check(err, IsNil)
|
||||
|
||||
c.Check(storage.HasPrefix([]byte("test")), Equals, false)
|
||||
c.Check(storage.KeysByPrefix([]byte("test")), DeepEquals, [][]byte{})
|
||||
c.Check(storage.FetchByPrefix([]byte("test")), DeepEquals, [][]byte{})
|
||||
|
||||
err = storage.ProcessByPrefix([]byte("test"), func(k, v []byte) error { return nil })
|
||||
c.Check(err, IsNil)
|
||||
|
||||
batch := storage.CreateBatch()
|
||||
c.Check(batch, NotNil)
|
||||
|
||||
tx, err := storage.OpenTransaction()
|
||||
c.Check(err, IsNil)
|
||||
c.Check(tx, NotNil)
|
||||
tx.Discard()
|
||||
|
||||
temp, err := storage.CreateTemporary()
|
||||
c.Check(err, IsNil)
|
||||
c.Check(temp, NotNil)
|
||||
temp.Close()
|
||||
temp.Drop()
|
||||
|
||||
err = storage.CompactDB()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
err = storage.Close()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
err = storage.Drop()
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
@@ -119,17 +119,17 @@ func (s *LevelDBSuite) TestDelete(c *C) {
|
||||
func (s *LevelDBSuite) TestByPrefix(c *C) {
|
||||
c.Check(s.db.FetchByPrefix([]byte{0x80}), DeepEquals, [][]byte{})
|
||||
|
||||
_ = s.db.Put([]byte{0x80, 0x01}, []byte{0x01})
|
||||
_ = s.db.Put([]byte{0x80, 0x03}, []byte{0x03})
|
||||
_ = s.db.Put([]byte{0x80, 0x02}, []byte{0x02})
|
||||
s.db.Put([]byte{0x80, 0x01}, []byte{0x01})
|
||||
s.db.Put([]byte{0x80, 0x03}, []byte{0x03})
|
||||
s.db.Put([]byte{0x80, 0x02}, []byte{0x02})
|
||||
c.Check(s.db.FetchByPrefix([]byte{0x80}), DeepEquals, [][]byte{{0x01}, {0x02}, {0x03}})
|
||||
c.Check(s.db.KeysByPrefix([]byte{0x80}), DeepEquals, [][]byte{{0x80, 0x01}, {0x80, 0x02}, {0x80, 0x03}})
|
||||
|
||||
_ = s.db.Put([]byte{0x90, 0x01}, []byte{0x04})
|
||||
s.db.Put([]byte{0x90, 0x01}, []byte{0x04})
|
||||
c.Check(s.db.FetchByPrefix([]byte{0x80}), DeepEquals, [][]byte{{0x01}, {0x02}, {0x03}})
|
||||
c.Check(s.db.KeysByPrefix([]byte{0x80}), DeepEquals, [][]byte{{0x80, 0x01}, {0x80, 0x02}, {0x80, 0x03}})
|
||||
|
||||
_ = s.db.Put([]byte{0x00, 0x01}, []byte{0x05})
|
||||
s.db.Put([]byte{0x00, 0x01}, []byte{0x05})
|
||||
c.Check(s.db.FetchByPrefix([]byte{0x80}), DeepEquals, [][]byte{{0x01}, {0x02}, {0x03}})
|
||||
c.Check(s.db.KeysByPrefix([]byte{0x80}), DeepEquals, [][]byte{{0x80, 0x01}, {0x80, 0x02}, {0x80, 0x03}})
|
||||
|
||||
@@ -161,7 +161,7 @@ func (s *LevelDBSuite) TestHasPrefix(c *C) {
|
||||
c.Check(s.db.HasPrefix([]byte(nil)), Equals, false)
|
||||
c.Check(s.db.HasPrefix([]byte{0x80}), Equals, false)
|
||||
|
||||
_ = s.db.Put([]byte{0x80, 0x01}, []byte{0x01})
|
||||
s.db.Put([]byte{0x80, 0x01}, []byte{0x01})
|
||||
|
||||
c.Check(s.db.HasPrefix([]byte(nil)), Equals, true)
|
||||
c.Check(s.db.HasPrefix([]byte{0x80}), Equals, true)
|
||||
@@ -180,8 +180,8 @@ func (s *LevelDBSuite) TestBatch(c *C) {
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
batch := s.db.CreateBatch()
|
||||
_ = batch.Put(key2, value2)
|
||||
_ = batch.Delete(key)
|
||||
batch.Put(key2, value2)
|
||||
batch.Delete(key)
|
||||
|
||||
v, err := s.db.Get(key)
|
||||
c.Check(err, IsNil)
|
||||
@@ -202,9 +202,9 @@ func (s *LevelDBSuite) TestBatch(c *C) {
|
||||
}
|
||||
|
||||
func (s *LevelDBSuite) TestCompactDB(c *C) {
|
||||
_ = s.db.Put([]byte{0x80, 0x01}, []byte{0x01})
|
||||
_ = s.db.Put([]byte{0x80, 0x03}, []byte{0x03})
|
||||
_ = s.db.Put([]byte{0x80, 0x02}, []byte{0x02})
|
||||
s.db.Put([]byte{0x80, 0x01}, []byte{0x01})
|
||||
s.db.Put([]byte{0x80, 0x03}, []byte{0x03})
|
||||
s.db.Put([]byte{0x80, 0x02}, []byte{0x02})
|
||||
|
||||
c.Check(s.db.CompactDB(), IsNil)
|
||||
}
|
||||
|
||||
@@ -32,9 +32,6 @@ func (s *storage) CreateTemporary() (database.Storage, error) {
|
||||
|
||||
// Get key value from database
|
||||
func (s *storage) Get(key []byte) ([]byte, error) {
|
||||
if s.db == nil {
|
||||
return nil, errors.New("database not open")
|
||||
}
|
||||
value, err := s.db.Get(key, nil)
|
||||
if err != nil {
|
||||
if err == leveldb.ErrNotFound {
|
||||
@@ -48,9 +45,6 @@ func (s *storage) Get(key []byte) ([]byte, error) {
|
||||
|
||||
// Put saves key to database, if key has the same value in DB already, it is not saved
|
||||
func (s *storage) Put(key []byte, value []byte) error {
|
||||
if s.db == nil {
|
||||
return errors.New("database not open")
|
||||
}
|
||||
old, err := s.db.Get(key, nil)
|
||||
if err != nil {
|
||||
if err != leveldb.ErrNotFound {
|
||||
@@ -66,17 +60,11 @@ func (s *storage) Put(key []byte, value []byte) error {
|
||||
|
||||
// Delete removes key from DB
|
||||
func (s *storage) Delete(key []byte) error {
|
||||
if s.db == nil {
|
||||
return errors.New("database not open")
|
||||
}
|
||||
return s.db.Delete(key, nil)
|
||||
}
|
||||
|
||||
// KeysByPrefix returns all keys that start with prefix
|
||||
func (s *storage) KeysByPrefix(prefix []byte) [][]byte {
|
||||
if s.db == nil {
|
||||
return nil
|
||||
}
|
||||
result := make([][]byte, 0, 20)
|
||||
|
||||
iterator := s.db.NewIterator(nil, nil)
|
||||
@@ -94,9 +82,6 @@ func (s *storage) KeysByPrefix(prefix []byte) [][]byte {
|
||||
|
||||
// FetchByPrefix returns all values with keys that start with prefix
|
||||
func (s *storage) FetchByPrefix(prefix []byte) [][]byte {
|
||||
if s.db == nil {
|
||||
return nil
|
||||
}
|
||||
result := make([][]byte, 0, 20)
|
||||
|
||||
iterator := s.db.NewIterator(nil, nil)
|
||||
@@ -114,9 +99,6 @@ func (s *storage) FetchByPrefix(prefix []byte) [][]byte {
|
||||
|
||||
// HasPrefix checks whether it can find any key with given prefix and returns true if one exists
|
||||
func (s *storage) HasPrefix(prefix []byte) bool {
|
||||
if s.db == nil {
|
||||
return false
|
||||
}
|
||||
iterator := s.db.NewIterator(nil, nil)
|
||||
defer iterator.Release()
|
||||
return iterator.Seek(prefix) && bytes.HasPrefix(iterator.Key(), prefix)
|
||||
@@ -125,9 +107,6 @@ func (s *storage) HasPrefix(prefix []byte) bool {
|
||||
// ProcessByPrefix iterates through all entries where key starts with prefix and calls
|
||||
// StorageProcessor on key value pair
|
||||
func (s *storage) ProcessByPrefix(prefix []byte, proc database.StorageProcessor) error {
|
||||
if s.db == nil {
|
||||
return errors.New("database not open")
|
||||
}
|
||||
iterator := s.db.NewIterator(nil, nil)
|
||||
defer iterator.Release()
|
||||
|
||||
@@ -164,9 +143,6 @@ func (s *storage) Open() error {
|
||||
|
||||
// CreateBatch creates a Batch object
|
||||
func (s *storage) CreateBatch() database.Batch {
|
||||
if s.db == nil {
|
||||
return nil
|
||||
}
|
||||
return &batch{
|
||||
db: s.db,
|
||||
b: &leveldb.Batch{},
|
||||
@@ -175,9 +151,6 @@ func (s *storage) CreateBatch() database.Batch {
|
||||
|
||||
// OpenTransaction creates new transaction.
|
||||
func (s *storage) OpenTransaction() (database.Transaction, error) {
|
||||
if s.db == nil {
|
||||
return nil, errors.New("database not open")
|
||||
}
|
||||
t, err := s.db.OpenTransaction()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -188,9 +161,6 @@ func (s *storage) OpenTransaction() (database.Transaction, error) {
|
||||
|
||||
// CompactDB compacts database by merging layers
|
||||
func (s *storage) CompactDB() error {
|
||||
if s.db == nil {
|
||||
return errors.New("database not open")
|
||||
}
|
||||
return s.db.CompactRange(util.Range{})
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user