Repository: bacalhau-project/bacalhau (language: python)

Issue:
Default install sometimes fails with unable to read auth tokens
```
root@vm-us-region-2-01:/home/ubuntu# bacalhau node list
23:33:08.481 | WRN cmd/util/api.go:52 > Failed to read access tokens – API calls will be without authorization error="failed to read tokens file: error decoding JSON: EOF" [stack:[{"func":"ReadToken","line":"56","source":"tokens.go"},{"func":"GetAPIClientV2","line":"50","source":"api.go"},{"func":"(*ListOptions).run","line":"66","source":"list.go"},{"func":"(*Command).execute","line":"987","source":"command.go"},{"func":"(*Command).ExecuteC","line":"1115","source":"command.go"},{"func":"(*Command).Execute","line":"1039","source":"command.go"},{"func":"Execute","line":"187","source":"root.go"},{"func":"main.func2","line":"90","source":"main.go"},{"func":"goexit","line":"1598","source":"asm_amd64.s"}]]
23:33:08.482 | WRN cmd/util/api.go:52 > Failed to read access tokens – API calls will be without authorization error="failed to read tokens file: error decoding JSON: EOF" [stack:[{"func":"ReadToken","line":"56","source":"tokens.go"},{"func":"GetAPIClientV2","line":"50","source":"api.go"},{"func":"StartUpdateCheck.func1","line":"22","source":"version.go"},{"func":"RunUpdateChecker.func1","line":"121","source":"update.go"},{"func":"RunUpdateChecker.func2","line":"161","source":"update.go"},{"func":"goexit","line":"1598","source":"asm_amd64.s"}]]
failed request: unable to persist new client credential: error decoding JSON: EOF
```

Vanilla install, running on the same machine - 1.2.2


Discussion / hints:
This looks more like auth failing. 
What is the content of `$BACALHAU_DIR/tokens.json`?

Fix patch:
diff --git a/cmd/util/tokens.go b/cmd/util/tokens.go
index 50d890c6e7..77c66f48a1 100644
--- a/cmd/util/tokens.go
+++ b/cmd/util/tokens.go
@@ -23,6 +23,11 @@ func readTokens(path string) (tokens, error) {
 	}
 	defer closer.CloseWithLogOnError(path, file)
 
+	// Treat an empty file as an empty map: #3569.
+	if info, err := file.Stat(); err == nil && info.Size() == 0 {
+		return map[string]string{}, nil
+	}
+
 	var t tokens
 	err = json.NewDecoder(file).Decode(&t)
 	if err != nil {


Test patch:
diff --git a/cmd/util/tokens_test.go b/cmd/util/tokens_test.go
new file mode 100644
index 0000000000..d548983694
--- /dev/null
+++ b/cmd/util/tokens_test.go
@@ -0,0 +1,123 @@
+//go:build unit || !integration
+
+package util
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/bacalhau-project/bacalhau/pkg/config"
+	"github.com/bacalhau-project/bacalhau/pkg/config/types"
+	"github.com/bacalhau-project/bacalhau/pkg/publicapi/apimodels"
+	"github.com/bacalhau-project/bacalhau/pkg/storage/util"
+	"github.com/stretchr/testify/require"
+)
+
+const (
+	testDomain          = "http://example.com:1234"
+	otherDomain         = "http://examples.com:1234"
+	testDomainOtherPort = "http://example.com:1235"
+)
+
+var exampleToken = apimodels.HTTPCredential{
+	Scheme: "Bearer",
+	Value:  "some-token",
+}
+
+func TestReadingEmptyTokensFile(t *testing.T) {
+	path := filepath.Join(t.TempDir(), "tokens.json")
+	config.SetValue(types.AuthTokensPath, path)
+
+	token, err := ReadToken(testDomain)
+	require.NoError(t, err)
+	require.Nil(t, token)
+}
+
+func TestTokenRoundtrip(t *testing.T) {
+	path := filepath.Join(t.TempDir(), "tokens.json")
+	config.SetValue(types.AuthTokensPath, path)
+
+	err := WriteToken(testDomain, &exampleToken)
+	require.NoError(t, err)
+
+	t.Run("read back same domain", func(t *testing.T) {
+		token, err := ReadToken(testDomain)
+		require.NoError(t, err)
+		require.Equal(t, exampleToken.String(), token.String())
+	})
+
+	t.Run("read other domain", func(t *testing.T) {
+		token, err := ReadToken(otherDomain)
+		require.NoError(t, err)
+		require.Nil(t, token)
+	})
+
+	t.Run("read other port", func(t *testing.T) {
+		token, err := ReadToken(testDomainOtherPort)
+		require.NoError(t, err)
+		require.Nil(t, token)
+	})
+}
+
+func TestReadTokenFromEmptyFile(t *testing.T) {
+	path := filepath.Join(t.TempDir(), "tokens.json")
+	config.SetValue(types.AuthTokensPath, path)
+
+	err := os.WriteFile(path, []byte{}, util.OS_USER_RW)
+	require.NoError(t, err)
+
+	token, err := ReadToken(testDomain)
+	require.NoError(t, err)
+	require.Nil(t, token)
+}
+
+func TestWriteTokenToEmptyFile(t *testing.T) {
+	path := filepath.Join(t.TempDir(), "tokens.json")
+	config.SetValue(types.AuthTokensPath, path)
+
+	err := os.WriteFile(path, []byte{}, util.OS_USER_RW)
+	require.NoError(t, err)
+
+	err = WriteToken(testDomain, &exampleToken)
+	require.NoError(t, err)
+}
+
+func TestWriteNilTokenIsValid(t *testing.T) {
+	path := filepath.Join(t.TempDir(), "tokens.json")
+	config.SetValue(types.AuthTokensPath, path)
+
+	err := WriteToken(testDomain, nil)
+	require.NoError(t, err)
+
+	info, err := os.Stat(path)
+	require.NoError(t, err)
+	require.NotZero(t, info.Size())
+
+	token, err := ReadToken(testDomain)
+	require.NoError(t, err)
+	require.Nil(t, token)
+}
+
+func TestWriteNilTokenDeletesToken(t *testing.T) {
+	path := filepath.Join(t.TempDir(), "tokens.json")
+	config.SetValue(types.AuthTokensPath, path)
+
+	err := WriteToken(testDomain, &exampleToken)
+	require.NoError(t, err)
+
+	token, err := ReadToken(testDomain)
+	require.NoError(t, err)
+	require.NotNil(t, token)
+
+	err = WriteToken(testDomain, nil)
+	require.NoError(t, err)
+
+	info, err := os.Stat(path)
+	require.NoError(t, err)
+	require.NotZero(t, info.Size())
+
+	token, err = ReadToken(testDomain)
+	require.NoError(t, err)
+	require.Nil(t, token)
+}