Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions internal/auth/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,16 @@ import (
"time"
)

// Credentials matches the schema used by the Gradle plugin (Phase 2.2 §5).
// CLI is the canonical writer; the Gradle plugin reads it.
// CredentialsVersion is the schema version we write. Must match what
// `grounds-push`'s CredentialResolver accepts (currently 1). Bump in
// lockstep when the schema changes.
const CredentialsVersion = 1

// Credentials matches the schema used by the Gradle plugin
// (groundsgg/grounds-push CredentialResolver.kt). CLI is the canonical
// writer; the Gradle plugin reads it.
type Credentials struct {
Version int `json:"version"`
AccessToken string `json:"accessToken"`
RefreshToken string `json:"refreshToken"`
ExpiresAt time.Time `json:"expiresAt"`
Expand All @@ -17,12 +24,24 @@ type Credentials struct {
PreferredUser string `json:"preferredUsername,omitempty"`
}

func (c *Credentials) Marshal() ([]byte, error) { return json.MarshalIndent(c, "", " ") }
// Marshal enforces the current schema version on every write so callers
// don't have to remember to set it. Files written before this field
// existed are silently upgraded on the next save (see ParseCredentials).
func (c *Credentials) Marshal() ([]byte, error) {
c.Version = CredentialsVersion
return json.MarshalIndent(c, "", " ")
}

func ParseCredentials(b []byte) (*Credentials, error) {
c := &Credentials{}
if err := json.Unmarshal(b, c); err != nil {
return nil, fmt.Errorf("parse credentials: %w", err)
}
// Legacy files written before the version field existed parse with
// Version == 0. Treat as v1 — they'll be re-written with the correct
// version on the next refresh / login.
if c.Version == 0 {
c.Version = CredentialsVersion
}
return c, nil
}
36 changes: 36 additions & 0 deletions internal/auth/credentials_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package auth

import (
"encoding/json"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
)
Expand Down Expand Up @@ -47,3 +49,37 @@ func TestLoadMissing(t *testing.T) {
t.Errorf("expected error on missing file")
}
}

// TestMarshalAlwaysWritesVersion verifies the cross-repo schema contract
// with grounds-push's CredentialResolver (which requires version: 1).
func TestMarshalAlwaysWritesVersion(t *testing.T) {
c := &Credentials{AccessToken: "at"}
blob, err := c.Marshal()
if err != nil {
t.Fatalf("Marshal: %v", err)
}
if !strings.Contains(string(blob), `"version": 1`) {
t.Errorf("missing version field in output: %s", blob)
}

var roundtrip Credentials
if err := json.Unmarshal(blob, &roundtrip); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if roundtrip.Version != CredentialsVersion {
t.Errorf("version = %d, want %d", roundtrip.Version, CredentialsVersion)
}
}

// TestParseLegacyFileWithoutVersion verifies that pre-version files
// written by an older CLI parse cleanly and get upgraded on next save.
func TestParseLegacyFileWithoutVersion(t *testing.T) {
legacy := []byte(`{"accessToken":"at","refreshToken":"rt"}`)
c, err := ParseCredentials(legacy)
if err != nil {
t.Fatalf("ParseCredentials: %v", err)
}
if c.Version != CredentialsVersion {
t.Errorf("legacy file should be upgraded to v%d, got v%d", CredentialsVersion, c.Version)
}
}
Loading