Skip to content

Commit df944d0

Browse files
authored
feat: environment variable from file (#3)
* feat(httpprobe): enhance environment variable handling * refactor(tests): Remove legacy validation, update random function
1 parent c865b8d commit df944d0

File tree

7 files changed

+176
-129
lines changed

7 files changed

+176
-129
lines changed

.gitignore

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,6 @@
44
CLAUDE.md
55
claude
66
bin
7-
logs
7+
logs
8+
test-results.json
9+
.env

cmd/httpprobe/run_cmd.go

+12-79
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,14 @@
11
package httpprobe
22

33
import (
4-
"fmt"
5-
"os"
6-
74
"github.com/mrfoh/httpprobe/internal/logging"
85
"github.com/mrfoh/httpprobe/internal/runner"
96
"github.com/mrfoh/httpprobe/internal/tests"
107
"github.com/mrfoh/httpprobe/pkg/easyreq"
11-
"github.com/olekukonko/tablewriter"
128
"github.com/spf13/cobra"
9+
"go.uber.org/zap"
1310
)
1411

15-
// displayResults outputs the test results in a formatted table grouped by test suite
16-
func displayResults(results map[string]tests.TestDefinitionExecResult) {
17-
var totalTests, passedTests int
18-
19-
// First, organize data by definition and suite
20-
for defName, defResult := range results {
21-
fmt.Printf("\n[1m%s[0m\n", defName)
22-
23-
for suiteName, suiteResult := range defResult.Suites {
24-
// Create a new table for each suite
25-
fmt.Printf("\n [1mSuite: %s[0m\n", suiteName)
26-
27-
table := tablewriter.NewWriter(os.Stdout)
28-
table.SetHeader([]string{"Test Case", "Status", "Time (s)"})
29-
table.SetBorder(false)
30-
table.SetHeaderColor(
31-
tablewriter.Colors{tablewriter.Bold},
32-
tablewriter.Colors{tablewriter.Bold},
33-
tablewriter.Colors{tablewriter.Bold},
34-
)
35-
36-
// Track suite statistics
37-
var suiteTotal, suitePassed int
38-
39-
// Add the test cases for this suite
40-
for caseName, caseResult := range suiteResult.Cases {
41-
totalTests++
42-
suiteTotal++
43-
44-
status := "FAIL"
45-
color := tablewriter.FgRedColor
46-
47-
if caseResult.Passed {
48-
status = "PASS"
49-
color = tablewriter.FgGreenColor
50-
passedTests++
51-
suitePassed++
52-
}
53-
54-
// Format the time with 2 decimal places
55-
timeStr := fmt.Sprintf("%.2f", caseResult.Timing)
56-
57-
table.Rich(
58-
[]string{caseName, status, timeStr},
59-
[]tablewriter.Colors{
60-
{},
61-
{color},
62-
{},
63-
},
64-
)
65-
}
66-
67-
table.Render()
68-
69-
// Print suite summary
70-
suitePassRate := 0.0
71-
if suiteTotal > 0 {
72-
suitePassRate = (float64(suitePassed) / float64(suiteTotal)) * 100
73-
}
74-
75-
fmt.Printf(" Suite Summary: %d/%d tests passed (%.1f%%)\n",
76-
suitePassed, suiteTotal, suitePassRate)
77-
}
78-
}
79-
80-
// Print overall summary
81-
overallPassRate := 0.0
82-
if totalTests > 0 {
83-
overallPassRate = (float64(passedTests) / float64(totalTests)) * 100
84-
}
85-
86-
fmt.Printf("\n[1mOverall Summary:[0m %d/%d tests passed (%.1f%%)\n",
87-
passedTests, totalTests, overallPassRate)
88-
}
89-
9012
func NewRunCmd() *cobra.Command {
9113
cmd := &cobra.Command{
9214
Use: "run",
@@ -98,6 +20,13 @@ func NewRunCmd() *cobra.Command {
9820
concurrency, _ := cmd.Flags().GetInt("concurrency")
9921
verbose, _ := cmd.Flags().GetBool("verbose")
10022
output, _ := cmd.Flags().GetString("output")
23+
envFile, _ := cmd.Flags().GetString("envfile")
24+
25+
// Load environment variables from file
26+
if err := tests.LoadEnvFile(envFile); err != nil {
27+
cmd.PrintErrf("Error loading environment variables: %v\n", err)
28+
return
29+
}
10130

10231
var loggingLevel string
10332

@@ -115,6 +44,10 @@ func NewRunCmd() *cobra.Command {
11544
return
11645
}
11746

47+
if envFile != "" {
48+
logger.Debug("Loaded environment variables from file", zap.String("file", envFile))
49+
}
50+
11851
httpClientOptions := easyreq.NewOptions().
11952
WithLogger(logger)
12053

docs/cli-usage.md

+57-9
Original file line numberDiff line numberDiff line change
@@ -166,26 +166,74 @@ httpprobe run --verbose
166166

167167
## Environment Variables
168168

169-
HttpProbe can load environment variables from a file specified with `--envfile`. These variables can be accessed in your test definitions using the `${ENV:VARIABLE_NAME}` syntax.
169+
HttpProbe can load environment variables from a file specified with `--envfile`. These variables can be accessed in your test definitions using the `${env:VARIABLE_NAME}` syntax.
170170

171-
For example, if your `.env` file contains:
171+
### Configuration
172172

173+
By default, HttpProbe looks for a file named `.env` in the current directory. You can specify a different file using the `--envfile` flag:
174+
175+
```bash
176+
httpprobe run --envfile .env.production
173177
```
178+
179+
### Environment File Format
180+
181+
The environment file should use the standard format of one variable per line, with `KEY=VALUE` pairs:
182+
183+
```
184+
# Comments are supported
174185
API_KEY=secret-key
175186
BASE_URL=https://api.example.com
187+
TIMEOUT=30
188+
```
189+
190+
Both single and double quotes are supported and will be stripped from the values:
191+
176192
```
193+
SECRET_KEY='this is a quoted string'
194+
API_TOKEN="another quoted string"
195+
```
196+
197+
### Using Environment Variables in Tests
177198

178-
You can reference these in your test definitions:
199+
Once loaded, environment variables can be accessed in your test definitions using the `${env:VARIABLE_NAME}` syntax:
179200

180201
```yaml
181-
- name: Get user profile
182-
request:
183-
url: ${ENV:BASE_URL}/profile
184-
headers:
185-
Authorization: Bearer ${ENV:API_KEY}
202+
name: API Tests with Environment Variables
203+
suites:
204+
- name: Authentication Tests
205+
cases:
206+
- title: Test API key authentication
207+
request:
208+
url: ${env:BASE_URL}/auth
209+
headers:
210+
- key: Authorization
211+
value: Bearer ${env:API_KEY}
212+
body:
213+
type: json
214+
Data:
215+
environment: ${env:ENV_NAME}
186216
```
187217
188-
This allows you to keep sensitive information out of your test files and change configurations based on environment.
218+
### Environment Switching
219+
220+
This feature is particularly useful for switching between different environments (development, staging, production) without modifying your test files:
221+
222+
```bash
223+
# Run tests against development environment
224+
httpprobe run --envfile .env.development
225+
226+
# Run tests against production environment
227+
httpprobe run --envfile .env.production
228+
```
229+
230+
### Best Practices
231+
232+
1. **Sensitive Information**: Use environment variables for API keys, tokens, and other sensitive data
233+
2. **Environment-specific URLs**: Store base URLs and endpoints in environment variables
234+
3. **Credentials**: Keep usernames, passwords, and other credentials in environment variables
235+
4. **Multiple Environments**: Create separate .env files for different environments (.env.dev, .env.staging, .env.prod)
236+
5. **CI/CD Integration**: In CI/CD pipelines, use the appropriate environment file for each environment
189237

190238
## Exit Codes
191239

internal/tests/suite.go

-36
Original file line numberDiff line numberDiff line change
@@ -168,39 +168,3 @@ func validateWithAssertions(resp *easyreq.HttpResponse, assertionsData map[strin
168168
return len(validationErrors) == 0, validationErrors, nil
169169
}
170170

171-
// Legacy validation method for backward compatibility
172-
func validateResponse(resp *easyreq.HttpResponse, assertions ResponseAssertion, logger logging.Logger) (bool, []error, error) {
173-
// Convert to new assertion format
174-
assertionsMap := make(map[string]interface{})
175-
176-
// Add status assertion
177-
if assertions.Status > 0 {
178-
assertionsMap["status"] = assertions.Status
179-
}
180-
181-
// Add header assertions
182-
if len(assertions.Headers) > 0 {
183-
headers := make(map[string]interface{})
184-
for _, assertion := range assertions.Headers {
185-
headers[assertion.Path] = assertion.Expected
186-
}
187-
assertionsMap["headers"] = headers
188-
}
189-
190-
// Add body assertions
191-
if len(assertions.Body) > 0 {
192-
body := make(map[string]interface{})
193-
for _, assertion := range assertions.Body {
194-
// Handle operators by prefixing the expected value
195-
if assertion.Operator != "" && assertion.Operator != "eq" && assertion.Operator != "equals" {
196-
expectedStr := fmt.Sprintf("%s %v", assertion.Operator, assertion.Expected)
197-
body[assertion.Path] = expectedStr
198-
} else {
199-
body[assertion.Path] = assertion.Expected
200-
}
201-
}
202-
assertionsMap["body"] = body
203-
}
204-
205-
return validateWithAssertions(resp, assertionsMap, logger)
206-
}

internal/tests/variables.go

+62-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package tests
22

33
import (
4+
"bufio"
45
"fmt"
56
"os"
67
"regexp"
@@ -10,9 +11,66 @@ import (
1011
"math/rand"
1112
)
1213

13-
// init initializes the random number generator
14-
func init() {
15-
rand.Seed(time.Now().UnixNano())
14+
// Define a global random source
15+
var randomSource = rand.NewSource(time.Now().UnixNano())
16+
var random = rand.New(randomSource)
17+
18+
// LoadEnvFile loads environment variables from a file
19+
// Environment variables in the file should be in the format KEY=VALUE
20+
func LoadEnvFile(filePath string) error {
21+
if filePath == "" {
22+
return nil
23+
}
24+
25+
file, err := os.Open(filePath)
26+
if err != nil {
27+
// If file doesn't exist, just return nil (not an error)
28+
if os.IsNotExist(err) {
29+
return nil
30+
}
31+
return fmt.Errorf("error opening env file: %w", err)
32+
}
33+
defer file.Close()
34+
35+
scanner := bufio.NewScanner(file)
36+
lineNum := 0
37+
38+
for scanner.Scan() {
39+
lineNum++
40+
line := scanner.Text()
41+
42+
// Skip empty lines and comments
43+
if line == "" || strings.HasPrefix(line, "#") {
44+
continue
45+
}
46+
47+
// Split on first equals sign
48+
parts := strings.SplitN(line, "=", 2)
49+
if len(parts) != 2 {
50+
return fmt.Errorf("invalid format in env file at line %d: %s", lineNum, line)
51+
}
52+
53+
key := strings.TrimSpace(parts[0])
54+
value := strings.TrimSpace(parts[1])
55+
56+
// Remove quotes if present
57+
if len(value) > 1 && (strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"")) ||
58+
(strings.HasPrefix(value, "'") && strings.HasSuffix(value, "'")) {
59+
value = value[1 : len(value)-1]
60+
}
61+
62+
// Set environment variable
63+
err := os.Setenv(key, value)
64+
if err != nil {
65+
return fmt.Errorf("error setting environment variable %s: %w", key, err)
66+
}
67+
}
68+
69+
if err := scanner.Err(); err != nil {
70+
return fmt.Errorf("error reading env file: %w", err)
71+
}
72+
73+
return nil
1674
}
1775

1876
// InterpolateVariables replaces variable references in the input string
@@ -91,7 +149,7 @@ func processRandomFunction(args []string) string {
91149
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
92150
result := make([]byte, length)
93151
for i := range result {
94-
result[i] = charset[rand.Intn(len(charset))]
152+
result[i] = charset[random.Intn(len(charset))]
95153
}
96154

97155
return string(result)

samples/.env.sample

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Production environment variables
2+
API_KEY=api-key-12345
3+
BASE_URL=https://httpbin.org
4+
ENV_NAME=sample

samples/with_env_variables.test.yaml

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: Sample with Environment Variables
2+
description: This test uses environment variables directly with ${env:VARIABLE_NAME} syntax
3+
suites:
4+
- name: Environment Variable Tests
5+
cases:
6+
- title: Using ENV variables directly in the request
7+
request:
8+
method: POST
9+
url: http://httpbin.org/post
10+
headers:
11+
- key: Authorization
12+
value: Bearer ${env:API_KEY}
13+
- key: X-Environment
14+
value: ${env:ENV_NAME}
15+
body:
16+
type: json
17+
data:
18+
baseUrl: ${env:BASE_URL}
19+
environment: ${env:ENV_NAME}
20+
assertions:
21+
status: 200
22+
headers:
23+
- path: content-type
24+
operator: eq
25+
expected: application/json
26+
body:
27+
- path: json.baseUrl
28+
operator: eq
29+
expected: https://api.example.com
30+
- path: json.environment
31+
operator: eq
32+
expected: development
33+
- path: headers.Authorization
34+
operator: eq
35+
expected: "Bearer test-api-key"
36+
- path: headers.X-Environment
37+
operator: eq
38+
expected: "development"

0 commit comments

Comments
 (0)