wafredir/test.go

116 lines
3.7 KiB
Go

package main
import (
"fmt"
"log"
"net/http"
"sync"
)
var sem chan Empty // semaphore to limit requess in flight
// test accepts a slice of type Redirect and returns an error
// it performs actual HTTP GET requests on each source URL and validates that a redirect occurs
// to the destination URL and that the redirect type/status code is correct
// This function makes use of concurrent requests to speed up testing.
func test(redirects []Redirect) error {
// This string will hold output for any failed tests. It's displayed when all tests have completed
var summaryOutput string
// Set up some tools to handle concurrency
wg := &sync.WaitGroup{} // Used to wait for all tests to finish
mu := &sync.Mutex{} // Used to lock shared memory in critical sections
sem = make(Semaphore, *maxConcurrentRequests) // Used to limit resources while having all requests queued up
// Loop through all redirects and queue them up
for _, redirect := range redirects {
wg.Add(1) // Add 1 resource to waitgroup
P(1) // Take 1 resource from semaphore
// This anonymous function executes in a separate go routine and can run concurrently
go func(redirect Redirect) {
defer V(1) // If function exits (error or otherwise), put 1 resource back into semaphore
defer wg.Done() // If function exits (error or otherwise), subtract 1 resource from waitgroup
// Create an HTTP client and override CheckRedirect to return the last response error so we can check the redirect type
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
// writing to std out is critical section
mu.Lock()
fmt.Printf("Checking redirect for: %s\n", redirect.sourceURL)
mu.Unlock()
// Make the request
resp, err := client.Get(redirect.sourceURL)
if err != nil {
log.Printf("HTTP GET failed for source URL '%s': %v", redirect.sourceURL, err)
}
// Check the status code
if resp.StatusCode != redirect.statusCode {
// Modifying summaryOutput is critical section
mu.Lock()
summaryOutput += fmt.Sprintf("redirect for source URL '%s': expected status code'%d': got '%d\n", redirect.sourceURL, redirect.statusCode, resp.StatusCode)
mu.Unlock()
return
}
// Parse response location URL from header into URL object
destURL, err := resp.Location()
if err != nil {
log.Printf("failed to parse response location to URL: %v", err)
}
// Check that the redirect went to the correct location
if destURL.String() != redirect.destinationURL {
// Modifying summyarOutput is critical section
mu.Lock()
summaryOutput += fmt.Sprintf("redirect for source URL '%s': expected '%s': got '%s\n", redirect.sourceURL, redirect.destinationURL, destURL.String())
mu.Unlock()
return
}
}(redirect)
}
// Wait for all tests to complete
wg.Wait()
fmt.Printf("\ndone tests.\n---------------------------------------------\n")
// Display summaryOutput if any tests failed
if len(summaryOutput) > 0 {
fmt.Printf("Summary:\n\n%s", summaryOutput)
} else {
fmt.Println("All redirect tests succeeded.")
}
return nil
}
// Semaphore helper functions
// Empty is an empty struct used by the semaphores
type Empty struct{}
// Semaphore is a channel which passes empty structs and acts as a resource lock
type Semaphore chan Empty
// P acquire n resources - standard semaphore design pattern to limit number of requests in flight
func P(n int) {
e := Empty{}
for i := 0; i < n; i++ {
sem <- e
}
}
// V release n resources - standard semaphore design pattern to limit number of requests in flight
func V(n int) {
for i := 0; i < n; i++ {
<-sem
}
}