Compare commits

..

2 Commits

Author SHA1 Message Date
bpiraeus 8fa1922477 minor cleanup based on make check 2025-02-15 23:49:45 +01:00
bpiraeus 836137f15d Adding authorisation options for API access
- ldap currently the only supported method
adding authorisation options for local repositories
  - ldap groups per repo
2025-02-15 23:49:45 +01:00
227 changed files with 2367 additions and 19547 deletions
+230 -1265
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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
+199 -294
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
+24 -38
View File
@@ -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)
}
-74
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+3 -3
View File
@@ -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
View File
@@ -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)
}
-267
View File
@@ -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
View File
@@ -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 {
-338
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
-381
View File
@@ -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&param=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
}
-600
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
-675
View File
@@ -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
View File
@@ -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 cant 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, dont 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, dont 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
})
}
-591
View File
@@ -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
View File
@@ -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
-18
View File
@@ -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/")
}
+1 -3
View File
@@ -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)
-18
View File
@@ -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
View File
@@ -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())
}
-496
View File
@@ -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)
}
-71
View File
@@ -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"))
}
-449
View File
@@ -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))
}
}
-31
View File
@@ -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
}
}
}
-716
View File
@@ -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
View File
@@ -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
-2
View File
@@ -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
-25
View File
@@ -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)
}
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -97,7 +97,7 @@ package environment to new version.`,
Flag: *flag.NewFlagSet("aptly", flag.ExitOnError),
Subcommands: []*commander.Command{
makeCmdConfig(),
makeCmdDB(),
makeCmdDb(),
makeCmdGraph(),
makeCmdMirror(),
makeCmdRepo(),
-88
View File
@@ -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
View File
@@ -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
View File
@@ -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
-240
View File
@@ -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))
}
}
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
})
+3 -3
View File
@@ -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)
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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()
+1 -1
View File
@@ -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)
}
+2
View File
@@ -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
}
+3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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)
}
+1 -1
View File
@@ -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
}
+2 -2
View File
@@ -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
View File
@@ -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
}
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -11,7 +11,7 @@ func Test(t *testing.T) {
TestingT(t)
}
type ProgressSuite struct{}
type ProgressSuite struct {}
var _ = Suite(&ProgressSuite{})
+12
View File
@@ -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
View File
@@ -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 {
-179
View File
@@ -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
}
}
}
}
-46
View File
@@ -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)
}
}
-210
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+11 -11
View File
@@ -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)
}
-99
View File
@@ -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)
}
-381
View File
@@ -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()
}
}
-384
View File
@@ -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)
}
-51
View File
@@ -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
View File
@@ -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)
}
-110
View File
@@ -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)
}
}
+3 -6
View File
@@ -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
+2 -2
View File
@@ -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)
}
+11 -11
View File
@@ -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)
}
-30
View File
@@ -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