Repository: nektos/act (language: go)

Issue:
--container-daemon-socket and DOCKER_HOST both required with Podman
### Bug report info

```plain text
❯ gh act --bug-report
act version:            0.2.50
GOOS:                   linux
GOARCH:                 amd64
NumCPU:                 8
Docker host:            unix:///run/user/1000/podman/podman.sock
Sockets found:
        /var/run/docker.sock(broken)
        $XDG_RUNTIME_DIR/docker.sock
        $XDG_RUNTIME_DIR/podman/podman.sock
Config files:           
        /home/tom/.actrc:
                -P ubuntu-latest=node:12.20.1-buster-slim
                -P ubuntu-20.04=node:12.20.1-buster-slim
                -P ubuntu-18.04=node:12.20.1-buster-slim
                -P ubuntu-16.04=node:12.20.1-stretch-slim
Build info:
        Go version:            go1.20.7
        Module path:           github.com/nektos/act
        Main version:          (devel)
        Main path:             github.com/nektos/act
        Main checksum:         
        Build settings:
                -buildmode:           exe
                -compiler:            gc
                -trimpath:            true
                CGO_ENABLED:          0
                GOARCH:               amd64
                GOOS:                 linux
                GOAMD64:              v1
                vcs:                  git
                vcs.revision:         e8856f0fb00fcdd16eef2325b845f55f5d346f51
                vcs.time:             2023-08-21T16:17:06Z
                vcs.modified:         true
Docker Engine:
        Engine version:        4.6.2
        Engine runtime:        crun
        Cgroup version:        2
        Cgroup driver:         systemd
        Storage driver:        overlay
        Registry URI:          
        OS:                    fedora
        OS type:               linux
        OS version:            38
        OS arch:               amd64
        OS kernel:             6.4.15-200.fc38.x86_64
        OS CPU:                8
        OS memory:             15687 MB
        Security options:
                name=seccomp,profile=default
                name=rootless
                name=selinux
```


### Command used with act

```sh
act --container-daemon-socket $XDG_RUNTIME_DIR/podman/podman.sock pull_request -e pull_request.json 

# or
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock
act pull_request -e pull_request.json
```


### Describe issue

When using Podman with the `podman.socket` service running, it is necessary to specify *both* `--container-daemon-socket` *and* `DOCKER_HOST`. It seems like it should be sufficient to only set `DOCKER_HOST` and for that socket to be used, without having to also pass `--container-daemon-socket`. 

Output with DOCKER_HOST unset:
```
❯ gh act --container-daemon-socket $XDG_RUNTIME_DIR/podman/podman.sock pull_request -e pull_request.json
[Asana/set-state-waiting] 🚀  Start image=node:12.20.1-buster-slim
[Asana/set-state-waiting]   🐳  docker pull image=node:12.20.1-buster-slim platform= username= forcePull=true
Error: permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Post "http://%2Fvar%2Frun%2Fdocker.sock/v1.24/images/create?fromImage=node&tag=12.20.1-buster-slim": dial unix /var/run/docker.sock: connect: permission denied
```

**N.B.** There is no additional log output when using `-v` in this case. 

---

Output with DOCKER_HOST set, no command line argument:
```
❯ export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock
❯ gh act pull_request -e pull_request.json
[Asana/set-state-waiting] 🚀  Start image=node:12.20.1-buster-slim
[Asana/set-state-waiting]   🐳  docker pull image=node:12.20.1-buster-slim platform= username= forcePull=true
[Asana/set-state-waiting]   🐳  docker create image=node:12.20.1-buster-slim platform= entrypoint=["tail" "-f" "/dev/null"] cmd=[]
Error: failed to create container: 'Error response from daemon: container create: statfs /var/run/docker.sock: permission denied'
```

With both the environment variable and the command line argument, the command runs successfully.

Verbose output for **this case** is shown below.



### Link to GitHub repository

_No response_

### Workflow content

```yml
name: Asana

on:
  pull_request:
    types: ["opened", "edited", "reopened", "synchronize", "ready_for_review"]

jobs:
  set-state-waiting:
    if: ${{ !github.event.pull_request.draft }}
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
```


### Relevant log output

```sh
[Asana/set-state-waiting] [DEBUG] pulling image 'docker.io/library/node:12.20.1-buster-slim' ()
DEBU[0000] Saving notices etag=16104d16-7646-4b54-86fe-f05680fde0a5 
DEBU[0000] No new notices                               
[Asana/set-state-waiting] [DEBUG] Already exists :: cd4902827248
[Asana/set-state-waiting] [DEBUG] Already exists :: 450331f0fd6d
[Asana/set-state-waiting] [DEBUG] Already exists :: 56f831d51b9a
[Asana/set-state-waiting] [DEBUG] Already exists :: d7361e2eb1cb
[Asana/set-state-waiting] [DEBUG] Already exists :: 45b42c59be33
[Asana/set-state-waiting] [DEBUG] Pulling fs layer :: 4ead7950876e
[Asana/set-state-waiting] [DEBUG] Download complete :: 4ead7950876e
[Asana/set-state-waiting] [DEBUG] Download complete :: 4ead7950876e
[Asana/set-state-waiting]   🐳  docker create image=node:12.20.1-buster-slim platform= entrypoint=["tail" "-f" "/dev/null"] cmd=[]
[Asana/set-state-waiting] [DEBUG] Common container.Config ==> &{Hostname: Domainname: User: AttachStdin:false AttachStdout:false AttachStderr:false ExposedPorts:map[] Tty:true OpenStdin:false StdinOnce:false Env:[RUNNER_TOOL_CACHE=/opt/hostedtoolcache RUNNER_OS=Linux RUNNER_ARCH=amd64 RUNNER_TEMP=/tmp LANG=C.UTF-8] Cmd:[] Healthcheck:<nil> ArgsEscaped:false Image:node:12.20.1-buster-slim Volumes:map[] WorkingDir:/home/tom/dev/nbycomp/asana-github-actions Entrypoint:[] NetworkDisabled:false MacAddress: OnBuild:[] Labels:map[] StopSignal: StopTimeout:<nil> Shell:[]}
[Asana/set-state-waiting] [DEBUG] Common container.HostConfig ==> &{Binds:[/var/run/docker.sock:/var/run/docker.sock] ContainerIDFile: LogConfig:{Type: Config:map[]} NetworkMode:host PortBindings:map[] RestartPolicy:{Name: MaximumRetryCount:0} AutoRemove:false VolumeDriver: VolumesFrom:[] ConsoleSize:[0 0] Annotations:map[] CapAdd:[] CapDrop:[] CgroupnsMode: DNS:[] DNSOptions:[] DNSSearch:[] ExtraHosts:[] GroupAdd:[] IpcMode: Cgroup: Links:[] OomScoreAdj:0 PidMode: Privileged:false PublishAllPorts:false ReadonlyRootfs:false SecurityOpt:[] StorageOpt:map[] Tmpfs:map[] UTSMode: UsernsMode: ShmSize:0 Sysctls:map[] Runtime: Isolation: Resources:{CPUShares:0 Memory:0 NanoCPUs:0 CgroupParent: BlkioWeight:0 BlkioWeightDevice:[] BlkioDeviceReadBps:[] BlkioDeviceWriteBps:[] BlkioDeviceReadIOps:[] BlkioDeviceWriteIOps:[] CPUPeriod:0 CPUQuota:0 CPURealtimePeriod:0 CPURealtimeRuntime:0 CpusetCpus: CpusetMems: Devices:[] DeviceCgroupRules:[] DeviceRequests:[] KernelMemory:0 KernelMemoryTCP:0 MemoryReservation:0 MemorySwap:0 MemorySwappiness:<nil> OomKillDisable:<nil> PidsLimit:<nil> Ulimits:[] CPUCount:0 CPUPercent:0 IOMaximumIOps:0 IOMaximumBandwidth:0} Mounts:[{Type:volume Source:act-toolcache Target:/toolcache ReadOnly:false Consistency: BindOptions:<nil> VolumeOptions:<nil> TmpfsOptions:<nil> ClusterOptions:<nil>} {Type:volume Source:act-Asana-set-state-waiting-9673d7f8d5ebf1d8a45653134007d1a04bfeeb01bae6f2e7b538729310d1ff6a-env Target:/var/run/act ReadOnly:false Consistency: BindOptions:<nil> VolumeOptions:<nil> TmpfsOptions:<nil> ClusterOptions:<nil>} {Type:volume Source:act-Asana-set-state-waiting-9673d7f8d5ebf1d8a45653134007d1a04bfeeb01bae6f2e7b538729310d1ff6a Target:/home/tom/dev/nbycomp/asana-github-actions ReadOnly:false Consistency: BindOptions:<nil> VolumeOptions:<nil> TmpfsOptions:<nil> ClusterOptions:<nil>}] MaskedPaths:[] ReadonlyPaths:[] Init:<nil>}
Error: failed to create container: 'Error response from daemon: container create: statfs /var/run/docker.sock: permission denied'
```


### Additional information

_No response_


Discussion / hints:

Fix patch:
diff --git a/cmd/root.go b/cmd/root.go
index 349e6ac418a..5e5cbbf9a3f 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -32,7 +32,7 @@ import (
 // Execute is the entry point to running the CLI
 func Execute(ctx context.Context, version string) {
 	input := new(Input)
-	var rootCmd = &cobra.Command{
+	rootCmd := &cobra.Command{
 		Use:               "act [event name to run] [flags]\n\nIf no event name passed, will default to \"on: push\"\nIf actions handles only one event it will be used as default instead of \"on: push\"",
 		Short:             "Run GitHub actions locally by specifying the event name (e.g. `push`) or an action name directly.",
 		Args:              cobra.MaximumNArgs(1),
@@ -125,34 +125,6 @@ func configLocations() []string {
 	return []string{specPath, homePath, invocationPath}
 }
 
-var commonSocketPaths = []string{
-	"/var/run/docker.sock",
-	"/run/podman/podman.sock",
-	"$HOME/.colima/docker.sock",
-	"$XDG_RUNTIME_DIR/docker.sock",
-	"$XDG_RUNTIME_DIR/podman/podman.sock",
-	`\\.\pipe\docker_engine`,
-	"$HOME/.docker/run/docker.sock",
-}
-
-// returns socket path or false if not found any
-func socketLocation() (string, bool) {
-	if dockerHost, exists := os.LookupEnv("DOCKER_HOST"); exists {
-		return dockerHost, true
-	}
-
-	for _, p := range commonSocketPaths {
-		if _, err := os.Lstat(os.ExpandEnv(p)); err == nil {
-			if strings.HasPrefix(p, `\\.\`) {
-				return "npipe://" + filepath.ToSlash(os.ExpandEnv(p)), true
-			}
-			return "unix://" + filepath.ToSlash(os.ExpandEnv(p)), true
-		}
-	}
-
-	return "", false
-}
-
 func args() []string {
 	actrc := configLocations()
 
@@ -185,7 +157,7 @@ func bugReport(ctx context.Context, version string) error {
 
 	report += sprintf("Docker host:", dockerHost)
 	report += fmt.Sprintln("Sockets found:")
-	for _, p := range commonSocketPaths {
+	for _, p := range container.CommonSocketLocations {
 		if _, err := os.Lstat(os.ExpandEnv(p)); err != nil {
 			continue
 		} else if _, err := os.Stat(os.ExpandEnv(p)); err != nil {
@@ -356,18 +328,6 @@ func parseMatrix(matrix []string) map[string]map[string]bool {
 	return matrixes
 }
 
-func isDockerHostURI(daemonPath string) bool {
-	if protoIndex := strings.Index(daemonPath, "://"); protoIndex != -1 {
-		scheme := daemonPath[:protoIndex]
-		if strings.IndexFunc(scheme, func(r rune) bool {
-			return (r < 'a' || r > 'z') && (r < 'A' || r > 'Z')
-		}) == -1 {
-			return true
-		}
-	}
-	return false
-}
-
 //nolint:gocyclo
 func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []string) error {
 	return func(cmd *cobra.Command, args []string) error {
@@ -378,27 +338,12 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
 		if ok, _ := cmd.Flags().GetBool("bug-report"); ok {
 			return bugReport(ctx, cmd.Version)
 		}
-
-		// Prefer DOCKER_HOST, don't override it
-		socketPath, hasDockerHost := os.LookupEnv("DOCKER_HOST")
-		if !hasDockerHost {
-			// a - in containerDaemonSocket means don't mount, preserve this value
-			// otherwise if input.containerDaemonSocket is a filepath don't use it as socketPath
-			skipMount := input.containerDaemonSocket == "-" || !isDockerHostURI(input.containerDaemonSocket)
-			if input.containerDaemonSocket != "" && !skipMount {
-				socketPath = input.containerDaemonSocket
-			} else {
-				socket, found := socketLocation()
-				if !found {
-					log.Errorln("daemon Docker Engine socket not found and containerDaemonSocket option was not set")
-				} else {
-					socketPath = socket
-				}
-				if !skipMount {
-					input.containerDaemonSocket = socketPath
-				}
-			}
-			os.Setenv("DOCKER_HOST", socketPath)
+		if ret, err := container.GetSocketAndHost(input.containerDaemonSocket); err != nil {
+			log.Warnf("Couldn't get a valid docker connection: %+v", err)
+		} else {
+			os.Setenv("DOCKER_HOST", ret.Host)
+			input.containerDaemonSocket = ret.Socket
+			log.Infof("Using docker host '%s', and daemon socket '%s'", ret.Host, ret.Socket)
 		}
 
 		if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" && input.containerArchitecture == "" {
diff --git a/pkg/container/docker_socket.go b/pkg/container/docker_socket.go
new file mode 100644
index 00000000000..8b3ed02410f
--- /dev/null
+++ b/pkg/container/docker_socket.go
@@ -0,0 +1,134 @@
+package container
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
+
+	log "github.com/sirupsen/logrus"
+)
+
+var CommonSocketLocations = []string{
+	"/var/run/docker.sock",
+	"/run/podman/podman.sock",
+	"$HOME/.colima/docker.sock",
+	"$XDG_RUNTIME_DIR/docker.sock",
+	"$XDG_RUNTIME_DIR/podman/podman.sock",
+	`\\.\pipe\docker_engine`,
+	"$HOME/.docker/run/docker.sock",
+}
+
+// returns socket URI or false if not found any
+func socketLocation() (string, bool) {
+	if dockerHost, exists := os.LookupEnv("DOCKER_HOST"); exists {
+		return dockerHost, true
+	}
+
+	for _, p := range CommonSocketLocations {
+		if _, err := os.Lstat(os.ExpandEnv(p)); err == nil {
+			if strings.HasPrefix(p, `\\.\`) {
+				return "npipe://" + filepath.ToSlash(os.ExpandEnv(p)), true
+			}
+			return "unix://" + filepath.ToSlash(os.ExpandEnv(p)), true
+		}
+	}
+
+	return "", false
+}
+
+// This function, `isDockerHostURI`, takes a string argument `daemonPath`. It checks if the
+// `daemonPath` is a valid Docker host URI. It does this by checking if the scheme of the URI (the
+// part before "://") contains only alphabetic characters. If it does, the function returns true,
+// indicating that the `daemonPath` is a Docker host URI. If it doesn't, or if the "://" delimiter
+// is not found in the `daemonPath`, the function returns false.
+func isDockerHostURI(daemonPath string) bool {
+	if protoIndex := strings.Index(daemonPath, "://"); protoIndex != -1 {
+		scheme := daemonPath[:protoIndex]
+		if strings.IndexFunc(scheme, func(r rune) bool {
+			return (r < 'a' || r > 'z') && (r < 'A' || r > 'Z')
+		}) == -1 {
+			return true
+		}
+	}
+	return false
+}
+
+type SocketAndHost struct {
+	Socket string
+	Host   string
+}
+
+func GetSocketAndHost(containerSocket string) (SocketAndHost, error) {
+	log.Debugf("Handling container host and socket")
+
+	// Prefer DOCKER_HOST, don't override it
+	dockerHost, hasDockerHost := socketLocation()
+	socketHost := SocketAndHost{Socket: containerSocket, Host: dockerHost}
+
+	// ** socketHost.Socket cases **
+	// Case 1: User does _not_ want to mount a daemon socket (passes a dash)
+	// Case 2: User passes a filepath to the socket; is that even valid?
+	// Case 3: User passes a valid socket; do nothing
+	// Case 4: User omitted the flag; set a sane default
+
+	// ** DOCKER_HOST cases **
+	// Case A: DOCKER_HOST is set; use it, i.e. do nothing
+	// Case B: DOCKER_HOST is empty; use sane defaults
+
+	// Set host for sanity's sake, when the socket isn't useful
+	if !hasDockerHost && (socketHost.Socket == "-" || !isDockerHostURI(socketHost.Socket) || socketHost.Socket == "") {
+		// Cases: 1B, 2B, 4B
+		socket, found := socketLocation()
+		socketHost.Host = socket
+		hasDockerHost = found
+	}
+
+	// A - (dash) in socketHost.Socket means don't mount, preserve this value
+	// otherwise if socketHost.Socket is a filepath don't use it as socket
+	// Exit early if we're in an invalid state (e.g. when no DOCKER_HOST and user supplied "-", a dash or omitted)
+	if !hasDockerHost && socketHost.Socket != "" && !isDockerHostURI(socketHost.Socket) {
+		// Cases: 1B, 2B
+		// Should we early-exit here, since there is no host nor socket to talk to?
+		return SocketAndHost{}, fmt.Errorf("DOCKER_HOST was not set, couldn't be found in the usual locations, and the container daemon socket ('%s') is invalid", socketHost.Socket)
+	}
+
+	// Default to DOCKER_HOST if set
+	if socketHost.Socket == "" && hasDockerHost {
+		// Cases: 4A
+		log.Debugf("Defaulting container socket to DOCKER_HOST")
+		socketHost.Socket = socketHost.Host
+	}
+	// Set sane default socket location if user omitted it
+	if socketHost.Socket == "" {
+		// Cases: 4B
+		socket, _ := socketLocation()
+		// socket is empty if it isn't found, so assignment here is at worst a no-op
+		log.Debugf("Defaulting container socket to default '%s'", socket)
+		socketHost.Socket = socket
+	}
+
+	// Exit if both the DOCKER_HOST and socket are fulfilled
+	if hasDockerHost {
+		// Cases: 1A, 2A, 3A, 4A
+		if !isDockerHostURI(socketHost.Socket) {
+			// Cases: 1A, 2A
+			log.Debugf("DOCKER_HOST is set, but socket is invalid '%s'", socketHost.Socket)
+		}
+		return socketHost, nil
+	}
+
+	// Set a sane DOCKER_HOST default if we can
+	if isDockerHostURI(socketHost.Socket) {
+		// Cases: 3B
+		log.Debugf("Setting DOCKER_HOST to container socket '%s'", socketHost.Socket)
+		socketHost.Host = socketHost.Socket
+		// Both DOCKER_HOST and container socket are valid; short-circuit exit
+		return socketHost, nil
+	}
+
+	// Here there is no DOCKER_HOST _and_ the supplied container socket is not a valid URI (either invalid or a file path)
+	// Cases: 2B <- but is already handled at the top
+	// I.e. this path should never be taken
+	return SocketAndHost{}, fmt.Errorf("no DOCKER_HOST and an invalid container socket '%s'", socketHost.Socket)
+}


Test patch:
diff --git a/pkg/container/docker_socket_test.go b/pkg/container/docker_socket_test.go
new file mode 100644
index 00000000000..0915a937b62
--- /dev/null
+++ b/pkg/container/docker_socket_test.go
@@ -0,0 +1,150 @@
+package container
+
+import (
+	"os"
+	"testing"
+
+	log "github.com/sirupsen/logrus"
+	assert "github.com/stretchr/testify/assert"
+)
+
+func init() {
+	log.SetLevel(log.DebugLevel)
+}
+
+var originalCommonSocketLocations = CommonSocketLocations
+
+func TestGetSocketAndHostWithSocket(t *testing.T) {
+	// Arrange
+	CommonSocketLocations = originalCommonSocketLocations
+	dockerHost := "unix:///my/docker/host.sock"
+	socketURI := "/path/to/my.socket"
+	os.Setenv("DOCKER_HOST", dockerHost)
+
+	// Act
+	ret, err := GetSocketAndHost(socketURI)
+
+	// Assert
+	assert.Nil(t, err)
+	assert.Equal(t, SocketAndHost{socketURI, dockerHost}, ret)
+}
+
+func TestGetSocketAndHostNoSocket(t *testing.T) {
+	// Arrange
+	dockerHost := "unix:///my/docker/host.sock"
+	os.Setenv("DOCKER_HOST", dockerHost)
+
+	// Act
+	ret, err := GetSocketAndHost("")
+
+	// Assert
+	assert.Nil(t, err)
+	assert.Equal(t, SocketAndHost{dockerHost, dockerHost}, ret)
+}
+
+func TestGetSocketAndHostOnlySocket(t *testing.T) {
+	// Arrange
+	socketURI := "/path/to/my.socket"
+	os.Unsetenv("DOCKER_HOST")
+	CommonSocketLocations = originalCommonSocketLocations
+	defaultSocket, defaultSocketFound := socketLocation()
+
+	// Act
+	ret, err := GetSocketAndHost(socketURI)
+
+	// Assert
+	assert.NoError(t, err, "Expected no error from GetSocketAndHost")
+	assert.Equal(t, true, defaultSocketFound, "Expected to find default socket")
+	assert.Equal(t, socketURI, ret.Socket, "Expected socket to match common location")
+	assert.Equal(t, defaultSocket, ret.Host, "Expected ret.Host to match default socket location")
+}
+
+func TestGetSocketAndHostDontMount(t *testing.T) {
+	// Arrange
+	CommonSocketLocations = originalCommonSocketLocations
+	dockerHost := "unix:///my/docker/host.sock"
+	os.Setenv("DOCKER_HOST", dockerHost)
+
+	// Act
+	ret, err := GetSocketAndHost("-")
+
+	// Assert
+	assert.Nil(t, err)
+	assert.Equal(t, SocketAndHost{"-", dockerHost}, ret)
+}
+
+func TestGetSocketAndHostNoHostNoSocket(t *testing.T) {
+	// Arrange
+	CommonSocketLocations = originalCommonSocketLocations
+	os.Unsetenv("DOCKER_HOST")
+	defaultSocket, found := socketLocation()
+
+	// Act
+	ret, err := GetSocketAndHost("")
+
+	// Assert
+	assert.Equal(t, true, found, "Expected a default socket to be found")
+	assert.Nil(t, err, "Expected no error from GetSocketAndHost")
+	assert.Equal(t, SocketAndHost{defaultSocket, defaultSocket}, ret, "Expected to match default socket location")
+}
+
+// Catch
+// > Your code breaks setting DOCKER_HOST if shouldMount is false.
+// > This happens if neither DOCKER_HOST nor --container-daemon-socket has a value, but socketLocation() returns a URI
+func TestGetSocketAndHostNoHostNoSocketDefaultLocation(t *testing.T) {
+	// Arrange
+	mySocketFile, tmpErr := os.CreateTemp("", "act-*.sock")
+	mySocket := mySocketFile.Name()
+	unixSocket := "unix://" + mySocket
+	defer os.RemoveAll(mySocket)
+	assert.NoError(t, tmpErr)
+	os.Unsetenv("DOCKER_HOST")
+
+	CommonSocketLocations = []string{mySocket}
+	defaultSocket, found := socketLocation()
+
+	// Act
+	ret, err := GetSocketAndHost("")
+
+	// Assert
+	assert.Equal(t, unixSocket, defaultSocket, "Expected default socket to match common socket location")
+	assert.Equal(t, true, found, "Expected default socket to be found")
+	assert.Nil(t, err, "Expected no error from GetSocketAndHost")
+	assert.Equal(t, SocketAndHost{unixSocket, unixSocket}, ret, "Expected to match default socket location")
+}
+
+func TestGetSocketAndHostNoHostInvalidSocket(t *testing.T) {
+	// Arrange
+	os.Unsetenv("DOCKER_HOST")
+	mySocket := "/my/socket/path.sock"
+	CommonSocketLocations = []string{"/unusual", "/socket", "/location"}
+	defaultSocket, found := socketLocation()
+
+	// Act
+	ret, err := GetSocketAndHost(mySocket)
+
+	// Assert
+	assert.Equal(t, false, found, "Expected no default socket to be found")
+	assert.Equal(t, "", defaultSocket, "Expected no default socket to be found")
+	assert.Equal(t, SocketAndHost{}, ret, "Expected to match default socket location")
+	assert.Error(t, err, "Expected an error in invalid state")
+}
+
+func TestGetSocketAndHostOnlySocketValidButUnusualLocation(t *testing.T) {
+	// Arrange
+	socketURI := "unix:///path/to/my.socket"
+	CommonSocketLocations = []string{"/unusual", "/location"}
+	os.Unsetenv("DOCKER_HOST")
+	defaultSocket, found := socketLocation()
+
+	// Act
+	ret, err := GetSocketAndHost(socketURI)
+
+	// Assert
+	// Default socket locations
+	assert.Equal(t, "", defaultSocket, "Expect default socket location to be empty")
+	assert.Equal(t, false, found, "Expected no default socket to be found")
+	// Sane default
+	assert.Nil(t, err, "Expect no error from GetSocketAndHost")
+	assert.Equal(t, socketURI, ret.Host, "Expect host to default to unusual socket")
+}