1071 lines
38 KiB
Go
1071 lines
38 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package auth
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/keychain"
|
|
)
|
|
|
|
func TestDefaultBlankCurrentRejectsSameCorpGlobalMirrorOwnedByExactIdentity(t *testing.T) {
|
|
originalResolveProfile := tokenResolveProfile
|
|
originalLoadOrganization := tokenLoadKeychainForCorpID
|
|
originalLoadGlobal := tokenLoadKeychain
|
|
t.Cleanup(func() {
|
|
tokenResolveProfile = originalResolveProfile
|
|
tokenLoadKeychainForCorpID = originalLoadOrganization
|
|
tokenLoadKeychain = originalLoadGlobal
|
|
})
|
|
|
|
const corpID = "corp_blank_default_guard"
|
|
tokenResolveProfile = func(string, string) (*Profile, error) {
|
|
return &Profile{Name: "Legacy Contractor", CorpID: corpID}, nil
|
|
}
|
|
tokenLoadKeychainForCorpID = func(string) (*TokenData, error) {
|
|
return nil, ErrTokenDataNotFound
|
|
}
|
|
tokenLoadKeychain = func() (*TokenData, error) {
|
|
return &TokenData{
|
|
AccessToken: "exact-global-token",
|
|
CorpID: corpID,
|
|
UserID: "different-exact-user",
|
|
}, nil
|
|
}
|
|
|
|
loaded, err := loadTokenDataForProfileLocked(t.TempDir(), "")
|
|
if !errors.Is(err, ErrTokenDataNotFound) {
|
|
t.Fatalf("loadTokenDataForProfileLocked() error = %v, want %v", err, ErrTokenDataNotFound)
|
|
}
|
|
if loaded != nil {
|
|
t.Fatalf("loadTokenDataForProfileLocked() = %#v, want nil", loaded)
|
|
}
|
|
}
|
|
|
|
func TestTokenLoadIsolationMatrix(t *testing.T) {
|
|
const (
|
|
corpID = "corp_token_load_matrix"
|
|
exactUserID = "matrix-user-a"
|
|
otherUserID = "matrix-user-b"
|
|
)
|
|
type profileKind string
|
|
type tokenSlot string
|
|
type uidRelation string
|
|
const (
|
|
blankProfile profileKind = "blank"
|
|
exactProfile profileKind = "exact"
|
|
identitySlot tokenSlot = "identity"
|
|
orgSlot tokenSlot = "organization"
|
|
globalSlot tokenSlot = "global"
|
|
emptyUID uidRelation = "empty"
|
|
sameUID uidRelation = "same"
|
|
differentUID uidRelation = "different"
|
|
)
|
|
|
|
var tests []struct {
|
|
name string
|
|
profile Profile
|
|
slot tokenSlot
|
|
tokenUserID string
|
|
wantAccepted bool
|
|
}
|
|
for _, kind := range []profileKind{blankProfile, exactProfile} {
|
|
profile := Profile{Name: string(kind) + " matrix profile", CorpID: corpID}
|
|
if kind == exactProfile {
|
|
profile.UserID = exactUserID
|
|
}
|
|
for _, slot := range []tokenSlot{identitySlot, orgSlot, globalSlot} {
|
|
for _, relation := range []uidRelation{emptyUID, sameUID, differentUID} {
|
|
tokenUserID := ""
|
|
switch relation {
|
|
case sameUID:
|
|
tokenUserID = profile.UserID
|
|
case differentUID:
|
|
tokenUserID = otherUserID
|
|
}
|
|
wantAccepted := false
|
|
switch slot {
|
|
case identitySlot:
|
|
wantAccepted = kind == exactProfile && tokenUserID == profile.UserID
|
|
case orgSlot, globalSlot:
|
|
wantAccepted = tokenUserID == profile.UserID
|
|
}
|
|
tests = append(tests, struct {
|
|
name string
|
|
profile Profile
|
|
slot tokenSlot
|
|
tokenUserID string
|
|
wantAccepted bool
|
|
}{
|
|
name: string(kind) + "/" + string(slot) + "/" + string(relation),
|
|
profile: profile,
|
|
slot: slot,
|
|
tokenUserID: tokenUserID,
|
|
wantAccepted: wantAccepted,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
if len(tests) != 18 {
|
|
t.Fatalf("token isolation matrix has %d cells, want 18", len(tests))
|
|
}
|
|
|
|
originalResolveProfile := tokenResolveProfile
|
|
originalLoadIdentity := tokenLoadKeychainIdentity
|
|
originalLoadOrganization := tokenLoadKeychainForCorpID
|
|
originalLoadGlobal := tokenLoadKeychain
|
|
originalSaveIdentity := tokenSaveKeychainForIdentity
|
|
t.Cleanup(func() {
|
|
tokenResolveProfile = originalResolveProfile
|
|
tokenLoadKeychainIdentity = originalLoadIdentity
|
|
tokenLoadKeychainForCorpID = originalLoadOrganization
|
|
tokenLoadKeychain = originalLoadGlobal
|
|
tokenSaveKeychainForIdentity = originalSaveIdentity
|
|
})
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
token := &TokenData{
|
|
AccessToken: "matrix-token-" + strings.ReplaceAll(tt.name, "/", "-"),
|
|
CorpID: corpID,
|
|
UserID: tt.tokenUserID,
|
|
}
|
|
tokenResolveProfile = func(string, string) (*Profile, error) {
|
|
profile := tt.profile
|
|
return &profile, nil
|
|
}
|
|
tokenLoadKeychainIdentity = func(string, string) (*TokenData, error) {
|
|
if tt.slot == identitySlot {
|
|
return token, nil
|
|
}
|
|
return nil, ErrTokenDataNotFound
|
|
}
|
|
tokenLoadKeychainForCorpID = func(string) (*TokenData, error) {
|
|
if tt.slot == orgSlot {
|
|
return token, nil
|
|
}
|
|
return nil, ErrTokenDataNotFound
|
|
}
|
|
tokenLoadKeychain = func() (*TokenData, error) {
|
|
if tt.slot == globalSlot {
|
|
return token, nil
|
|
}
|
|
return nil, ErrTokenDataNotFound
|
|
}
|
|
tokenSaveKeychainForIdentity = func(string, string, *TokenData) error { return nil }
|
|
|
|
loaded, err := loadTokenDataForProfileLocked(t.TempDir(), "")
|
|
if tt.wantAccepted {
|
|
if err != nil || loaded == nil || loaded.AccessToken != token.AccessToken {
|
|
t.Fatalf("matrix cell accepted = false: loaded=%#v err=%v", loaded, err)
|
|
}
|
|
return
|
|
}
|
|
if err == nil || loaded != nil {
|
|
t.Fatalf("matrix cell accepted = true: loaded=%#v err=%v", loaded, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Slot names do not make a token from another organization trustworthy.
|
|
for _, profile := range []Profile{
|
|
{Name: "blank wrong-corp matrix profile", CorpID: corpID},
|
|
{Name: "exact wrong-corp matrix profile", CorpID: corpID, UserID: exactUserID},
|
|
} {
|
|
t.Run(profile.Name, func(t *testing.T) {
|
|
tokenResolveProfile = func(string, string) (*Profile, error) {
|
|
selected := profile
|
|
return &selected, nil
|
|
}
|
|
tokenLoadKeychainIdentity = func(string, string) (*TokenData, error) {
|
|
return &TokenData{AccessToken: "wrong-corp", CorpID: "corp_other", UserID: profile.UserID}, nil
|
|
}
|
|
tokenLoadKeychainForCorpID = func(string) (*TokenData, error) {
|
|
return &TokenData{AccessToken: "wrong-corp", CorpID: "corp_other", UserID: profile.UserID}, nil
|
|
}
|
|
tokenLoadKeychain = func() (*TokenData, error) { return nil, ErrTokenDataNotFound }
|
|
if loaded, err := loadTokenDataForProfileLocked(t.TempDir(), ""); err == nil || loaded != nil {
|
|
t.Fatalf("wrong-corp token accepted: loaded=%#v err=%v", loaded, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTokenPersistenceWritePlanKeepsOrganizationOwnershipIsolated(t *testing.T) {
|
|
const (
|
|
corpID = "corp_write_plan"
|
|
resolvedUID = "resolved-user"
|
|
coexistingID = "coexisting-user"
|
|
)
|
|
tests := []struct {
|
|
name string
|
|
cfg *ProfilesConfig
|
|
data *TokenData
|
|
runtimeSelector string
|
|
wantUpgrade bool
|
|
wantOrganization bool
|
|
wantMakeCurrent bool
|
|
}{
|
|
{
|
|
name: "explicit reauth completes sole unresolved profile",
|
|
cfg: &ProfilesConfig{
|
|
Version: profilesVersion,
|
|
Profiles: []Profile{{Name: "Historical Worker", CorpID: corpID}},
|
|
},
|
|
data: &TokenData{CorpID: corpID, UserID: resolvedUID},
|
|
runtimeSelector: corpID,
|
|
wantUpgrade: true,
|
|
wantOrganization: true,
|
|
},
|
|
{
|
|
name: "coexisting exact identity cannot overwrite unresolved owner",
|
|
cfg: &ProfilesConfig{
|
|
Version: profilesUnresolvedSelectorVersion,
|
|
Profiles: []Profile{
|
|
{Name: "Historical Worker", CorpID: corpID},
|
|
{Name: "Exact Worker", CorpID: corpID, UserID: coexistingID},
|
|
},
|
|
},
|
|
data: &TokenData{CorpID: corpID, UserID: coexistingID},
|
|
runtimeSelector: profileSelector(corpID, coexistingID),
|
|
wantOrganization: false,
|
|
},
|
|
{
|
|
name: "default exact login without unresolved owner updates organization",
|
|
cfg: &ProfilesConfig{
|
|
Version: profilesVersion,
|
|
Profiles: []Profile{{Name: "Exact Worker", CorpID: corpID, UserID: resolvedUID}},
|
|
},
|
|
data: &TokenData{CorpID: corpID, UserID: resolvedUID},
|
|
wantOrganization: true,
|
|
wantMakeCurrent: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
plan := planTokenPersistenceWrites(tt.cfg, tt.data, tt.runtimeSelector)
|
|
if !plan.WriteIdentity || !plan.WriteGlobal {
|
|
t.Fatalf("write plan = %#v, want identity and global slots included", plan)
|
|
}
|
|
if plan.UpgradesLegacyProfile != tt.wantUpgrade {
|
|
t.Fatalf("UpgradesLegacyProfile = %v, want %v", plan.UpgradesLegacyProfile, tt.wantUpgrade)
|
|
}
|
|
if plan.WriteOrganization != tt.wantOrganization {
|
|
t.Fatalf("WriteOrganization = %v, want %v", plan.WriteOrganization, tt.wantOrganization)
|
|
}
|
|
if plan.MakeCurrent != tt.wantMakeCurrent {
|
|
t.Fatalf("MakeCurrent = %v, want %v", plan.MakeCurrent, tt.wantMakeCurrent)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFreshUIDLessExactLoginCannotOverwriteExistingUnresolvedProfile(t *testing.T) {
|
|
fixture := seedLegacyBlankAndExactIdentitySlots(t)
|
|
exactSelector := profileSelector(fixture.corpID, fixture.alpha.UserID)
|
|
previousRuntimeProfile := RuntimeProfile()
|
|
SetRuntimeProfile(exactSelector)
|
|
t.Cleanup(func() { SetRuntimeProfile(previousRuntimeProfile) })
|
|
|
|
freshUnknown := testToken("fresh-unknown-exact-target", fixture.corpID, fixture.blank.CorpName)
|
|
freshUnknown.UserID = ""
|
|
freshUnknown.UserName = ""
|
|
freshUnknown.LegacyOrgScopedProfile = exactSelector
|
|
freshUnknown.FreshAuthorization = true
|
|
|
|
if err := preflightTokenWritePersistence(fixture.configDir, freshUnknown); err == nil ||
|
|
!strings.Contains(err.Error(), "refusing to save UID-less token") {
|
|
t.Fatalf("preflightTokenWritePersistence() error = %v, want unresolved-sibling protection", err)
|
|
}
|
|
if err := SaveTokenData(fixture.configDir, freshUnknown); err == nil ||
|
|
!strings.Contains(err.Error(), "refusing to save UID-less token") {
|
|
t.Fatalf("SaveTokenData() error = %v, want unresolved-sibling protection", err)
|
|
}
|
|
|
|
assertOrganizationTokenAccessForTest(t, fixture.corpID, fixture.blank.AccessToken, "")
|
|
assertIdentityTokenAccessForTest(t, fixture.corpID, fixture.alpha.UserID, fixture.alpha.AccessToken)
|
|
assertIdentityTokenAccessForTest(t, fixture.corpID, fixture.beta.UserID, fixture.beta.AccessToken)
|
|
|
|
// The same fresh authorization is also ambiguous without --profile. It must
|
|
// not inherit or overwrite whichever same-corp account happens to be current.
|
|
SetRuntimeProfile("")
|
|
freshUnknown.LegacyOrgScopedProfile = ""
|
|
if err := preflightTokenWritePersistence(fixture.configDir, freshUnknown); err == nil ||
|
|
!strings.Contains(err.Error(), "fresh UID-less token") {
|
|
t.Fatalf("implicit preflight error = %v, want multi-account unresolved protection", err)
|
|
}
|
|
if err := SaveLoginTokenData(fixture.configDir, freshUnknown); err == nil ||
|
|
!strings.Contains(err.Error(), "fresh UID-less token") {
|
|
t.Fatalf("implicit SaveLoginTokenData() error = %v, want multi-account unresolved protection", err)
|
|
}
|
|
assertOrganizationTokenAccessForTest(t, fixture.corpID, fixture.blank.AccessToken, "")
|
|
assertIdentityTokenAccessForTest(t, fixture.corpID, fixture.alpha.UserID, fixture.alpha.AccessToken)
|
|
|
|
// Explicitly targeting the unresolved account remains a supported reauth
|
|
// path for external workers whose contact response still has no userId.
|
|
cfg, err := LoadProfiles(fixture.configDir)
|
|
if err != nil {
|
|
t.Fatalf("LoadProfiles() error = %v", err)
|
|
}
|
|
unresolved := unresolvedProfileForCorp(cfg, fixture.corpID)
|
|
if unresolved == nil {
|
|
t.Fatal("unresolved profile missing")
|
|
}
|
|
unresolvedSelector := storedProfileSelector(cfg, unresolved)
|
|
SetRuntimeProfile(unresolvedSelector)
|
|
freshUnknown.LegacyOrgScopedProfile = unresolvedSelector
|
|
if err := SaveTokenData(fixture.configDir, freshUnknown); err != nil {
|
|
t.Fatalf("SaveTokenData(explicit unresolved reauth) error = %v", err)
|
|
}
|
|
assertOrganizationTokenAccessForTest(t, fixture.corpID, freshUnknown.AccessToken, "")
|
|
}
|
|
|
|
func TestCanonicalStoredSelectorKeepsCorpIDPriorityOverLegacyLocalName(t *testing.T) {
|
|
const (
|
|
organizationCorpID = "corp_selector_priority"
|
|
legacyCorpID = "corp_legacy_local_name"
|
|
)
|
|
cfg := &ProfilesConfig{
|
|
Version: profilesVersion,
|
|
Profiles: []Profile{
|
|
{
|
|
Name: "Organization Account",
|
|
CorpID: organizationCorpID,
|
|
CorpName: "Organization",
|
|
UserID: "organization-user",
|
|
},
|
|
{
|
|
Name: organizationCorpID,
|
|
CorpID: legacyCorpID,
|
|
CorpName: "Legacy Organization",
|
|
},
|
|
{
|
|
Name: "Legacy Exact Account",
|
|
CorpID: legacyCorpID,
|
|
CorpName: "Legacy Organization",
|
|
UserID: "legacy-exact-user",
|
|
},
|
|
},
|
|
}
|
|
|
|
want := profileSelector(organizationCorpID, "organization-user")
|
|
if got := canonicalStoredSelector(cfg, organizationCorpID); got != want {
|
|
t.Fatalf("canonicalStoredSelector(CorpID conflict) = %q, want organization selector %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestCanonicalStoredSelectorKeepsCorpNamePriorityOverLegacyLocalName(t *testing.T) {
|
|
const (
|
|
organizationCorpID = "corp_name_priority"
|
|
legacyCorpID = "corp_legacy_corp_name"
|
|
organizationName = "Shared Organization Name"
|
|
)
|
|
cfg := &ProfilesConfig{
|
|
Version: profilesVersion,
|
|
Profiles: []Profile{
|
|
{
|
|
Name: "Organization Account",
|
|
CorpID: organizationCorpID,
|
|
CorpName: organizationName,
|
|
UserID: "organization-user",
|
|
},
|
|
{
|
|
Name: organizationName,
|
|
CorpID: legacyCorpID,
|
|
CorpName: "Legacy Organization",
|
|
},
|
|
{
|
|
Name: "Legacy Exact Account",
|
|
CorpID: legacyCorpID,
|
|
CorpName: "Legacy Organization",
|
|
UserID: "legacy-exact-user",
|
|
},
|
|
},
|
|
}
|
|
|
|
want := profileSelector(organizationCorpID, "organization-user")
|
|
if got := canonicalStoredSelector(cfg, organizationName); got != want {
|
|
t.Fatalf("canonicalStoredSelector(CorpName conflict) = %q, want organization selector %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestCanonicalStoredSelectorRecoversUncontestedLegacyLocalName(t *testing.T) {
|
|
const (
|
|
corpID = "corp_uncontested_local_name"
|
|
localName = "Historical External Worker"
|
|
)
|
|
cfg := &ProfilesConfig{
|
|
Version: profilesVersion,
|
|
Profiles: []Profile{
|
|
{
|
|
Name: localName,
|
|
CorpID: corpID,
|
|
CorpName: "Local Name Organization",
|
|
},
|
|
{
|
|
Name: "Exact Account",
|
|
CorpID: corpID,
|
|
CorpName: "Local Name Organization",
|
|
UserID: "exact-user",
|
|
},
|
|
},
|
|
}
|
|
|
|
got := canonicalStoredSelector(cfg, localName)
|
|
if got != localName {
|
|
t.Fatalf("canonicalStoredSelector(uncontested local name) = %q, want %q", got, localName)
|
|
}
|
|
selected, exact, err := resolveProfileSelection("", cfg, got)
|
|
if err != nil {
|
|
t.Fatalf("resolveProfileSelection(recovered local name) error = %v", err)
|
|
}
|
|
if !exact || selected == nil || selected.CorpID != corpID || selected.UserID != "" {
|
|
t.Fatalf("recovered local selector resolved to %#v exact=%v, want unresolved profile", selected, exact)
|
|
}
|
|
}
|
|
|
|
func TestSetCurrentProfileRejectsUnreadableExactIdentityBesideBlankProfile(t *testing.T) {
|
|
fixture := installUnreadableExactProfileSelectionFixture(t, false)
|
|
|
|
selected, err := setCurrentProfileLocked(fixture.configDir, fixture.unreadableSelector)
|
|
if !errors.Is(err, fixture.unreadableErr) {
|
|
t.Fatalf("setCurrentProfileLocked() = %#v, %v; want unreadable token error", selected, err)
|
|
}
|
|
fixture.assertUnchanged(t)
|
|
}
|
|
|
|
func TestUsePreviousProfileRejectsUnreadableExactIdentityBesideBlankProfile(t *testing.T) {
|
|
fixture := installUnreadableExactProfileSelectionFixture(t, true)
|
|
|
|
selected, err := usePreviousProfileLocked(fixture.configDir)
|
|
if !errors.Is(err, fixture.unreadableErr) {
|
|
t.Fatalf("usePreviousProfileLocked() = %#v, %v; want unreadable token error", selected, err)
|
|
}
|
|
fixture.assertUnchanged(t)
|
|
}
|
|
|
|
func TestSetCurrentProfileRejectsMismatchedExactIdentitySlotBesideBlankProfile(t *testing.T) {
|
|
fixture := installUnreadableExactProfileSelectionFixture(t, false)
|
|
fixture.unreadableToken = &TokenData{
|
|
AccessToken: "readable-but-wrong-identity-token",
|
|
CorpID: fixture.corpID,
|
|
UserID: "another-exact-user",
|
|
}
|
|
|
|
selected, err := setCurrentProfileLocked(fixture.configDir, fixture.unreadableSelector)
|
|
if err == nil || !strings.Contains(err.Error(), "does not match") {
|
|
t.Fatalf("setCurrentProfileLocked() = %#v, %v; want identity mismatch error", selected, err)
|
|
}
|
|
fixture.assertUnchanged(t)
|
|
}
|
|
|
|
func TestReservedUnresolvedSelectorPromotesSchemaToDowngradeGuardVersion(t *testing.T) {
|
|
const wantDowngradeGuardVersion = 3
|
|
configDir := t.TempDir()
|
|
corpID := "corp_reserved_schema_guard"
|
|
reserved := unresolvedProfileSelector(corpID)
|
|
cfg := &ProfilesConfig{
|
|
Version: profilesVersion,
|
|
CurrentProfile: reserved,
|
|
Profiles: []Profile{
|
|
{Name: "Reserved Organization", CorpID: corpID, CorpName: "Reserved Organization"},
|
|
{Name: "Exact Account", CorpID: corpID, CorpName: "Reserved Organization", UserID: "exact-user"},
|
|
},
|
|
}
|
|
|
|
if err := SaveProfiles(configDir, cfg); err != nil {
|
|
t.Fatalf("SaveProfiles() error = %v", err)
|
|
}
|
|
loaded, err := LoadProfiles(configDir)
|
|
if err != nil {
|
|
t.Fatalf("LoadProfiles() error = %v", err)
|
|
}
|
|
if loaded.Version != wantDowngradeGuardVersion {
|
|
t.Fatalf("profiles version = %d, want downgrade guard version %d", loaded.Version, wantDowngradeGuardVersion)
|
|
}
|
|
if loaded.CurrentProfile != reserved {
|
|
t.Fatalf("current profile = %q, want %q", loaded.CurrentProfile, reserved)
|
|
}
|
|
if err := ensureProfilesWritable(loaded); err != nil {
|
|
t.Fatalf("current client rejected reserved-selector schema: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestV2ProfilesWithoutReservedSelectorRemainDowngradeReadable(t *testing.T) {
|
|
configDir := t.TempDir()
|
|
cfg := &ProfilesConfig{
|
|
Version: profilesVersion,
|
|
CurrentProfile: "corp_v2_safe:user_v2_safe",
|
|
Profiles: []Profile{{
|
|
Name: "V2 Safe Account",
|
|
CorpID: "corp_v2_safe",
|
|
UserID: "user_v2_safe",
|
|
}},
|
|
}
|
|
|
|
if err := SaveProfiles(configDir, cfg); err != nil {
|
|
t.Fatalf("SaveProfiles() error = %v", err)
|
|
}
|
|
loaded, err := LoadProfiles(configDir)
|
|
if err != nil {
|
|
t.Fatalf("LoadProfiles() error = %v", err)
|
|
}
|
|
if loaded.Version != profilesVersion {
|
|
t.Fatalf("ordinary profiles version = %d, want v2 compatibility version %d", loaded.Version, profilesVersion)
|
|
}
|
|
}
|
|
|
|
func TestInvalidReservedOrgCurrentSelectorNormalizesBackToV2(t *testing.T) {
|
|
configDir := t.TempDir()
|
|
corpID := "corp_reserved_org_current_guard"
|
|
cfg := &ProfilesConfig{
|
|
Version: profilesVersion,
|
|
CurrentProfile: profileSelector(corpID, "exact-user"),
|
|
OrgCurrentProfiles: map[string]string{
|
|
corpID: unresolvedProfileSelector(corpID),
|
|
},
|
|
Profiles: []Profile{
|
|
{Name: "Reserved Organization", CorpID: corpID, CorpName: "Reserved Organization"},
|
|
{Name: "Exact Account", CorpID: corpID, CorpName: "Reserved Organization", UserID: "exact-user"},
|
|
},
|
|
}
|
|
|
|
if err := SaveProfiles(configDir, cfg); err != nil {
|
|
t.Fatalf("SaveProfiles() error = %v", err)
|
|
}
|
|
raw, err := os.ReadFile(ProfilesPath(configDir))
|
|
if err != nil {
|
|
t.Fatalf("ReadFile(profiles.json) error = %v", err)
|
|
}
|
|
var persisted struct {
|
|
Version int `json:"version"`
|
|
OrgCurrentProfiles map[string]string `json:"orgCurrentProfiles"`
|
|
}
|
|
if err := json.Unmarshal(raw, &persisted); err != nil {
|
|
t.Fatalf("json.Unmarshal(profiles.json) error = %v", err)
|
|
}
|
|
if persisted.Version != profilesVersion {
|
|
t.Fatalf("persisted profiles version = %d, want %d", persisted.Version, profilesVersion)
|
|
}
|
|
if _, exists := persisted.OrgCurrentProfiles[corpID]; exists {
|
|
t.Fatalf("invalid organization-current selector survived normalization: %#v", persisted.OrgCurrentProfiles)
|
|
}
|
|
}
|
|
|
|
func TestEnsureMigrationPersistsDowngradeGuardForRawV2ReservedSelector(t *testing.T) {
|
|
const wantDowngradeGuardVersion = 3
|
|
t.Setenv(keychain.DisableKeychainEnv, "1")
|
|
cleanupKeychain(t)
|
|
configDir := t.TempDir()
|
|
corpID := "corp_raw_v2_reserved_guard"
|
|
cfg := &ProfilesConfig{
|
|
Version: profilesVersion,
|
|
CurrentProfile: unresolvedProfileSelector(corpID),
|
|
Profiles: []Profile{
|
|
{Name: "Raw V2 Organization", CorpID: corpID, CorpName: "Raw V2 Organization"},
|
|
{Name: "Exact Account", CorpID: corpID, CorpName: "Raw V2 Organization", UserID: "exact-user"},
|
|
},
|
|
}
|
|
writeRawProfilesFixture(t, configDir, cfg)
|
|
|
|
if err := EnsureProfilesMigration(configDir); err != nil {
|
|
t.Fatalf("EnsureProfilesMigration() error = %v", err)
|
|
}
|
|
if got := readRawProfilesVersion(t, configDir); got != wantDowngradeGuardVersion {
|
|
t.Fatalf("persisted profiles version = %d, want %d", got, wantDowngradeGuardVersion)
|
|
}
|
|
}
|
|
|
|
func TestV1ReservedSelectorMigrationStillRunsBeforeDowngradeGuardPromotion(t *testing.T) {
|
|
const wantDowngradeGuardVersion = 3
|
|
t.Setenv(keychain.DisableKeychainEnv, "1")
|
|
cleanupKeychain(t)
|
|
configDir := t.TempDir()
|
|
corpID := "corp_raw_v1_reserved_guard"
|
|
cfg := &ProfilesConfig{
|
|
Version: 1,
|
|
CurrentProfile: unresolvedProfileSelector(corpID),
|
|
Profiles: []Profile{
|
|
{Name: "Raw V1 Organization", CorpID: corpID, CorpName: "Raw V1 Organization"},
|
|
{Name: "Exact Account", CorpID: corpID, CorpName: "Raw V1 Organization", UserID: "exact-user"},
|
|
},
|
|
}
|
|
writeRawProfilesFixture(t, configDir, cfg)
|
|
|
|
if err := EnsureProfilesMigration(configDir); err != nil {
|
|
t.Fatalf("EnsureProfilesMigration() error = %v", err)
|
|
}
|
|
if got := readRawProfilesVersion(t, configDir); got != wantDowngradeGuardVersion {
|
|
t.Fatalf("persisted profiles version = %d, want %d", got, wantDowngradeGuardVersion)
|
|
}
|
|
}
|
|
|
|
func TestV1MigrationStillPromotesOnlyToIdentitySchema(t *testing.T) {
|
|
t.Setenv(keychain.DisableKeychainEnv, "1")
|
|
cleanupKeychain(t)
|
|
configDir := t.TempDir()
|
|
token := testToken("legacy-v1-token", "corp_v1_schema", "V1 Schema Organization")
|
|
token.UserID = "legacy-v1-user"
|
|
if err := SaveTokenDataKeychainForCorpID(token.CorpID, token); err != nil {
|
|
t.Fatalf("SaveTokenDataKeychainForCorpID() error = %v", err)
|
|
}
|
|
if err := SaveProfiles(configDir, &ProfilesConfig{
|
|
Version: 1,
|
|
CurrentProfile: profileSelector(token.CorpID, token.UserID),
|
|
Profiles: []Profile{{
|
|
Name: token.CorpName,
|
|
CorpID: token.CorpID,
|
|
CorpName: token.CorpName,
|
|
UserID: token.UserID,
|
|
}},
|
|
}); err != nil {
|
|
t.Fatalf("SaveProfiles(v1 fixture) error = %v", err)
|
|
}
|
|
|
|
if err := EnsureProfilesMigration(configDir); err != nil {
|
|
t.Fatalf("EnsureProfilesMigration() error = %v", err)
|
|
}
|
|
loaded, err := LoadProfiles(configDir)
|
|
if err != nil {
|
|
t.Fatalf("LoadProfiles() error = %v", err)
|
|
}
|
|
if loaded.Version != profilesVersion {
|
|
t.Fatalf("migrated profiles version = %d, want identity schema %d", loaded.Version, profilesVersion)
|
|
}
|
|
if !TokenDataExistsKeychainForIdentity(token.CorpID, token.UserID) {
|
|
t.Fatal("v1 migration did not create the exact identity token slot")
|
|
}
|
|
}
|
|
|
|
func TestExplicitReauthUpgradingSoleBlankProfilePublishesAllTokenMirrors(t *testing.T) {
|
|
t.Setenv(keychain.DisableKeychainEnv, "1")
|
|
cleanupKeychain(t)
|
|
configDir := t.TempDir()
|
|
legacy := testToken("legacy-blank-access", "corp_explicit_reauth", "Explicit Reauth Organization")
|
|
legacy.UserID = ""
|
|
legacy.UserName = ""
|
|
if err := SaveTokenData(configDir, legacy); err != nil {
|
|
t.Fatalf("SaveTokenData(legacy blank) error = %v", err)
|
|
}
|
|
|
|
SetRuntimeProfile(legacy.CorpID)
|
|
t.Cleanup(func() { SetRuntimeProfile("") })
|
|
reauthorized := *legacy
|
|
reauthorized.AccessToken = "reauthorized-exact-access"
|
|
reauthorized.RefreshToken = "reauthorized-exact-refresh"
|
|
reauthorized.UserID = "resolved-external-worker"
|
|
reauthorized.UserName = "Resolved External Worker"
|
|
if err := preflightTokenWritePersistence(configDir, &reauthorized); err != nil {
|
|
t.Fatalf("preflightTokenWritePersistence(explicit reauth) error = %v", err)
|
|
}
|
|
if err := SaveTokenData(configDir, &reauthorized); err != nil {
|
|
t.Fatalf("SaveTokenData(explicit reauth) error = %v", err)
|
|
}
|
|
|
|
assertIdentityTokenAccessForTest(t, reauthorized.CorpID, reauthorized.UserID, reauthorized.AccessToken)
|
|
assertOrganizationTokenAccessForTest(t, reauthorized.CorpID, reauthorized.AccessToken, reauthorized.UserID)
|
|
global, err := LoadTokenDataKeychain()
|
|
if err != nil {
|
|
t.Fatalf("LoadTokenDataKeychain() error = %v", err)
|
|
}
|
|
if global.AccessToken != reauthorized.AccessToken || global.UserID != reauthorized.UserID {
|
|
t.Fatalf("global token after explicit reauth = %#v, want reauthorized exact token", global)
|
|
}
|
|
cfg, err := LoadProfiles(configDir)
|
|
if err != nil {
|
|
t.Fatalf("LoadProfiles() error = %v", err)
|
|
}
|
|
if len(cfg.Profiles) != 1 || cfg.Profiles[0].UserID != reauthorized.UserID {
|
|
t.Fatalf("profiles after explicit reauth = %#v, want one resolved identity", cfg.Profiles)
|
|
}
|
|
}
|
|
|
|
func TestExplicitReauthLegacyUpgradeRejectsUnreadableOrganizationMirror(t *testing.T) {
|
|
t.Setenv(keychain.DisableKeychainEnv, "1")
|
|
cleanupKeychain(t)
|
|
configDir := t.TempDir()
|
|
legacy := testToken("legacy-corrupt-org-access", "corp_explicit_reauth_corrupt", "Corrupt Reauth Organization")
|
|
legacy.UserID = ""
|
|
legacy.UserName = ""
|
|
if err := SaveTokenData(configDir, legacy); err != nil {
|
|
t.Fatalf("SaveTokenData(legacy blank) error = %v", err)
|
|
}
|
|
if err := keychain.Set(keychain.Service, TokenAccountForCorpID(legacy.CorpID), "{unreadable"); err != nil {
|
|
t.Fatalf("write unreadable organization mirror: %v", err)
|
|
}
|
|
|
|
SetRuntimeProfile(legacy.CorpID)
|
|
t.Cleanup(func() { SetRuntimeProfile("") })
|
|
reauthorized := *legacy
|
|
reauthorized.AccessToken = "must-not-persist"
|
|
reauthorized.RefreshToken = "must-not-persist-refresh"
|
|
reauthorized.UserID = "resolved-external-worker"
|
|
reauthorized.UserName = "Resolved External Worker"
|
|
err := preflightTokenWritePersistence(configDir, &reauthorized)
|
|
if err == nil || !strings.Contains(err.Error(), "profile token slot") ||
|
|
!strings.Contains(err.Error(), "parse token data") {
|
|
t.Fatalf("preflightTokenWritePersistence() error = %v, want unreadable organization mirror", err)
|
|
}
|
|
if TokenDataExistsKeychainForIdentity(reauthorized.CorpID, reauthorized.UserID) {
|
|
t.Fatal("rejected explicit reauth created an identity token")
|
|
}
|
|
global, loadErr := LoadTokenDataKeychain()
|
|
if loadErr != nil {
|
|
t.Fatalf("LoadTokenDataKeychain() error = %v", loadErr)
|
|
}
|
|
if global.AccessToken != legacy.AccessToken || global.UserID != "" {
|
|
t.Fatalf("rejected explicit reauth changed global token: %#v", global)
|
|
}
|
|
}
|
|
|
|
func TestV3DowngradesAfterBlankProfileIdentityCompletion(t *testing.T) {
|
|
t.Setenv(keychain.DisableKeychainEnv, "1")
|
|
cleanupKeychain(t)
|
|
configDir := t.TempDir()
|
|
corpID := "corp_v3_identity_completion"
|
|
resolved := testToken("resolved-org-access", corpID, "V3 Completion Organization")
|
|
resolved.UserID = "resolved-user"
|
|
resolved.UserName = "Resolved User"
|
|
if err := SaveTokenDataKeychainForCorpID(corpID, resolved); err != nil {
|
|
t.Fatalf("SaveTokenDataKeychainForCorpID() error = %v", err)
|
|
}
|
|
writeRawProfilesFixture(t, configDir, &ProfilesConfig{
|
|
Version: profilesUnresolvedSelectorVersion,
|
|
CurrentProfile: unresolvedProfileSelector(corpID),
|
|
Profiles: []Profile{{
|
|
Name: "Historical External Worker",
|
|
CorpID: corpID,
|
|
CorpName: resolved.CorpName,
|
|
}},
|
|
})
|
|
|
|
if err := EnsureProfilesMigration(configDir); err != nil {
|
|
t.Fatalf("EnsureProfilesMigration() error = %v", err)
|
|
}
|
|
cfg, err := LoadProfiles(configDir)
|
|
if err != nil {
|
|
t.Fatalf("LoadProfiles() error = %v", err)
|
|
}
|
|
if cfg.Version != profilesVersion {
|
|
t.Fatalf("profiles version after identity completion = %d, want %d", cfg.Version, profilesVersion)
|
|
}
|
|
if len(cfg.Profiles) != 1 || cfg.Profiles[0].UserID != resolved.UserID {
|
|
t.Fatalf("profiles after identity completion = %#v", cfg.Profiles)
|
|
}
|
|
if strings.Contains(string(readRawProfilesForTest(t, configDir)), unresolvedProfileSelectorPrefix) {
|
|
t.Fatal("identity completion left reserved selector grammar on disk")
|
|
}
|
|
}
|
|
|
|
func TestV3DowngradesAfterUnresolvedProfileDeletion(t *testing.T) {
|
|
t.Setenv(keychain.DisableKeychainEnv, "1")
|
|
fixture := seedBlankProfileSelectorFixture(t, "Deletion Organization", "Deletion Organization", true)
|
|
cfg, err := LoadProfiles(fixture.configDir)
|
|
if err != nil {
|
|
t.Fatalf("LoadProfiles() error = %v", err)
|
|
}
|
|
cfg.Version = profilesUnresolvedSelectorVersion
|
|
if err := SaveProfiles(fixture.configDir, cfg); err != nil {
|
|
t.Fatalf("SaveProfiles(v3 fixture) error = %v", err)
|
|
}
|
|
if err := DeleteTokenDataForProfile(fixture.configDir, fixture.blankSelector); err != nil {
|
|
t.Fatalf("DeleteTokenDataForProfile(blank) error = %v", err)
|
|
}
|
|
|
|
cfg, err = LoadProfiles(fixture.configDir)
|
|
if err != nil {
|
|
t.Fatalf("LoadProfiles(after delete) error = %v", err)
|
|
}
|
|
if cfg.Version != profilesVersion {
|
|
t.Fatalf("profiles version after blank delete = %d, want %d", cfg.Version, profilesVersion)
|
|
}
|
|
if len(cfg.Profiles) != 1 || cfg.Profiles[0].UserID != fixture.exactUserID {
|
|
t.Fatalf("profiles after blank delete = %#v, want exact identity only", cfg.Profiles)
|
|
}
|
|
}
|
|
|
|
func TestV3DowngradesWhenExactConflictDeletionLeavesSoleBlankProfile(t *testing.T) {
|
|
t.Setenv(keychain.DisableKeychainEnv, "1")
|
|
fixture := seedBlankProfileSelectorFixture(
|
|
t,
|
|
"Conflict Organization",
|
|
"Conflict Organization",
|
|
true,
|
|
)
|
|
cfg, err := LoadProfiles(fixture.configDir)
|
|
if err != nil {
|
|
t.Fatalf("LoadProfiles() error = %v", err)
|
|
}
|
|
cfg.Version = profilesUnresolvedSelectorVersion
|
|
cfg.CurrentProfile = fixture.blankSelector
|
|
cfg.PreviousProfile = fixture.exactSelector
|
|
if err := SaveProfiles(fixture.configDir, cfg); err != nil {
|
|
t.Fatalf("SaveProfiles(v3 conflict fixture) error = %v", err)
|
|
}
|
|
if got := readRawProfilesVersion(t, fixture.configDir); got != profilesUnresolvedSelectorVersion {
|
|
t.Fatalf("fixture profiles version = %d, want %d", got, profilesUnresolvedSelectorVersion)
|
|
}
|
|
|
|
if err := DeleteTokenDataForProfile(fixture.configDir, fixture.exactSelector); err != nil {
|
|
t.Fatalf("DeleteTokenDataForProfile(exact conflict) error = %v", err)
|
|
}
|
|
|
|
cfg, err = LoadProfiles(fixture.configDir)
|
|
if err != nil {
|
|
t.Fatalf("LoadProfiles(after exact delete) error = %v", err)
|
|
}
|
|
if cfg.Version != profilesVersion {
|
|
t.Fatalf("profiles version after exact conflict delete = %d, want %d", cfg.Version, profilesVersion)
|
|
}
|
|
if cfg.CurrentProfile != fixture.corpID {
|
|
t.Fatalf("current profile after exact conflict delete = %q, want corpId %q", cfg.CurrentProfile, fixture.corpID)
|
|
}
|
|
if len(cfg.Profiles) != 1 ||
|
|
cfg.Profiles[0].CorpID != fixture.corpID ||
|
|
cfg.Profiles[0].UserID != "" {
|
|
t.Fatalf("profiles after exact conflict delete = %#v, want sole unresolved profile", cfg.Profiles)
|
|
}
|
|
if strings.Contains(string(readRawProfilesForTest(t, fixture.configDir)), unresolvedProfileSelectorPrefix) {
|
|
t.Fatal("exact conflict deletion left reserved selector grammar on disk")
|
|
}
|
|
loaded, err := LoadTokenDataForProfile(fixture.configDir, "")
|
|
if err != nil {
|
|
t.Fatalf("LoadTokenDataForProfile(current blank) error = %v", err)
|
|
}
|
|
if loaded.AccessToken != fixture.blankToken.AccessToken || loaded.UserID != "" {
|
|
t.Fatalf("current token after exact conflict delete = %#v, want unresolved token %#v", loaded, fixture.blankToken)
|
|
}
|
|
}
|
|
|
|
func TestV3DowngradesWhenReservedSelectorCanonicalizesToSafeLocalName(t *testing.T) {
|
|
t.Setenv(keychain.DisableKeychainEnv, "1")
|
|
cleanupKeychain(t)
|
|
configDir := t.TempDir()
|
|
corpID := "corp_v3_safe_selector"
|
|
const safeName = "Historical External Worker"
|
|
writeRawProfilesFixture(t, configDir, &ProfilesConfig{
|
|
Version: profilesUnresolvedSelectorVersion,
|
|
CurrentProfile: unresolvedProfileSelector(corpID),
|
|
Profiles: []Profile{
|
|
{Name: safeName, CorpID: corpID, CorpName: "Safe Selector Organization"},
|
|
{Name: "Exact Account", CorpID: corpID, CorpName: "Safe Selector Organization", UserID: "exact-user"},
|
|
},
|
|
})
|
|
|
|
if err := EnsureProfilesMigration(configDir); err != nil {
|
|
t.Fatalf("EnsureProfilesMigration() error = %v", err)
|
|
}
|
|
cfg, err := LoadProfiles(configDir)
|
|
if err != nil {
|
|
t.Fatalf("LoadProfiles() error = %v", err)
|
|
}
|
|
if cfg.Version != profilesVersion || cfg.CurrentProfile != safeName {
|
|
t.Fatalf("normalized profiles = version %d current %q, want v%d current %q", cfg.Version, cfg.CurrentProfile, profilesVersion, safeName)
|
|
}
|
|
if strings.Contains(string(readRawProfilesForTest(t, configDir)), unresolvedProfileSelectorPrefix) {
|
|
t.Fatal("safe selector normalization left reserved grammar on disk")
|
|
}
|
|
}
|
|
|
|
type unreadableExactProfileSelectionFixture struct {
|
|
configDir string
|
|
corpID string
|
|
unreadableSelector string
|
|
unreadableErr error
|
|
unreadableToken *TokenData
|
|
state *ProfilesConfig
|
|
before *ProfilesConfig
|
|
organizationToken *TokenData
|
|
globalToken *TokenData
|
|
beforeOrganization *TokenData
|
|
beforeGlobal *TokenData
|
|
}
|
|
|
|
func installUnreadableExactProfileSelectionFixture(t *testing.T, unreadableIsPrevious bool) *unreadableExactProfileSelectionFixture {
|
|
t.Helper()
|
|
|
|
originalEnsureMigration := profilesEnsureMigration
|
|
originalLoad := profilesLoad
|
|
originalSave := profilesSave
|
|
originalLoadIdentity := profilesLoadIdentity
|
|
originalLoadOrganization := profilesLoadCorp
|
|
originalSaveOrganization := profilesSaveCorp
|
|
originalDeleteOrganization := profilesDeleteCorp
|
|
originalLoadGlobal := profilesLoadLegacy
|
|
originalSaveGlobal := profilesSaveLegacy
|
|
originalDeleteGlobal := profilesDeleteLegacy
|
|
t.Cleanup(func() {
|
|
profilesEnsureMigration = originalEnsureMigration
|
|
profilesLoad = originalLoad
|
|
profilesSave = originalSave
|
|
profilesLoadIdentity = originalLoadIdentity
|
|
profilesLoadCorp = originalLoadOrganization
|
|
profilesSaveCorp = originalSaveOrganization
|
|
profilesDeleteCorp = originalDeleteOrganization
|
|
profilesLoadLegacy = originalLoadGlobal
|
|
profilesSaveLegacy = originalSaveGlobal
|
|
profilesDeleteLegacy = originalDeleteGlobal
|
|
})
|
|
|
|
const (
|
|
corpID = "corp_profile_switch_guard"
|
|
unreadableUID = "unreadable-exact-user"
|
|
currentUID = "readable-current-user"
|
|
)
|
|
unreadableSelector := profileSelector(corpID, unreadableUID)
|
|
currentSelector := profileSelector(corpID, currentUID)
|
|
previousSelector := "Legacy Contractor"
|
|
if unreadableIsPrevious {
|
|
previousSelector = unreadableSelector
|
|
}
|
|
state := &ProfilesConfig{
|
|
Version: profilesVersion,
|
|
CurrentProfile: currentSelector,
|
|
PreviousProfile: previousSelector,
|
|
OrgCurrentProfiles: map[string]string{
|
|
corpID: currentSelector,
|
|
},
|
|
Profiles: []Profile{
|
|
{Name: "Legacy Contractor", CorpID: corpID},
|
|
{Name: "Unreadable Account", CorpID: corpID, UserID: unreadableUID},
|
|
{Name: "Readable Account", CorpID: corpID, UserID: currentUID},
|
|
},
|
|
}
|
|
organizationToken := &TokenData{AccessToken: "legacy-organization-token", CorpID: corpID}
|
|
globalToken := &TokenData{AccessToken: "readable-global-token", CorpID: corpID, UserID: currentUID}
|
|
fixture := &unreadableExactProfileSelectionFixture{
|
|
configDir: t.TempDir(),
|
|
corpID: corpID,
|
|
unreadableSelector: unreadableSelector,
|
|
unreadableErr: errors.New("decrypt exact identity token: key mismatch"),
|
|
state: cloneProfilesConfig(state),
|
|
before: cloneProfilesConfig(state),
|
|
organizationToken: cloneTokenDataForTest(organizationToken),
|
|
globalToken: cloneTokenDataForTest(globalToken),
|
|
beforeOrganization: cloneTokenDataForTest(organizationToken),
|
|
beforeGlobal: cloneTokenDataForTest(globalToken),
|
|
}
|
|
|
|
profilesEnsureMigration = func(string) error { return nil }
|
|
profilesLoad = func(string) (*ProfilesConfig, error) {
|
|
return cloneProfilesConfig(fixture.state), nil
|
|
}
|
|
profilesSave = func(_ string, cfg *ProfilesConfig) error {
|
|
fixture.state = cloneProfilesConfig(cfg)
|
|
return nil
|
|
}
|
|
profilesLoadIdentity = func(_ string, userID string) (*TokenData, error) {
|
|
switch userID {
|
|
case unreadableUID:
|
|
if fixture.unreadableToken != nil {
|
|
return cloneTokenDataForTest(fixture.unreadableToken), nil
|
|
}
|
|
return nil, fixture.unreadableErr
|
|
case currentUID:
|
|
return cloneTokenDataForTest(fixture.globalToken), nil
|
|
default:
|
|
return nil, ErrTokenDataNotFound
|
|
}
|
|
}
|
|
profilesLoadCorp = func(string) (*TokenData, error) {
|
|
return cloneTokenDataForTest(fixture.organizationToken), nil
|
|
}
|
|
profilesSaveCorp = func(_ string, data *TokenData) error {
|
|
fixture.organizationToken = cloneTokenDataForTest(data)
|
|
return nil
|
|
}
|
|
profilesDeleteCorp = func(string) error {
|
|
fixture.organizationToken = nil
|
|
return nil
|
|
}
|
|
profilesLoadLegacy = func() (*TokenData, error) {
|
|
if fixture.globalToken == nil {
|
|
return nil, ErrTokenDataNotFound
|
|
}
|
|
return cloneTokenDataForTest(fixture.globalToken), nil
|
|
}
|
|
profilesSaveLegacy = func(data *TokenData) error {
|
|
fixture.globalToken = cloneTokenDataForTest(data)
|
|
return nil
|
|
}
|
|
profilesDeleteLegacy = func() error {
|
|
fixture.globalToken = nil
|
|
return nil
|
|
}
|
|
|
|
return fixture
|
|
}
|
|
|
|
func (f *unreadableExactProfileSelectionFixture) assertUnchanged(t *testing.T) {
|
|
t.Helper()
|
|
if !reflect.DeepEqual(f.state, f.before) {
|
|
t.Fatalf("profiles changed after rejected switch:\n got: %#v\nwant: %#v", f.state, f.before)
|
|
}
|
|
if !reflect.DeepEqual(f.organizationToken, f.beforeOrganization) {
|
|
t.Fatalf("organization mirror changed after rejected switch: got %#v want %#v", f.organizationToken, f.beforeOrganization)
|
|
}
|
|
if !reflect.DeepEqual(f.globalToken, f.beforeGlobal) {
|
|
t.Fatalf("global mirror changed after rejected switch: got %#v want %#v", f.globalToken, f.beforeGlobal)
|
|
}
|
|
}
|
|
|
|
func cloneTokenDataForTest(data *TokenData) *TokenData {
|
|
if data == nil {
|
|
return nil
|
|
}
|
|
clone := *data
|
|
return &clone
|
|
}
|
|
|
|
func writeRawProfilesFixture(t *testing.T, configDir string, cfg *ProfilesConfig) {
|
|
t.Helper()
|
|
raw, err := json.MarshalIndent(cfg, "", " ")
|
|
if err != nil {
|
|
t.Fatalf("json.MarshalIndent(profiles fixture) error = %v", err)
|
|
}
|
|
if err := os.WriteFile(ProfilesPath(configDir), append(raw, '\n'), 0o600); err != nil {
|
|
t.Fatalf("WriteFile(profiles fixture) error = %v", err)
|
|
}
|
|
}
|
|
|
|
func readRawProfilesVersion(t *testing.T, configDir string) int {
|
|
t.Helper()
|
|
raw, err := os.ReadFile(ProfilesPath(configDir))
|
|
if err != nil {
|
|
t.Fatalf("ReadFile(profiles.json) error = %v", err)
|
|
}
|
|
var persisted struct {
|
|
Version int `json:"version"`
|
|
}
|
|
if err := json.Unmarshal(raw, &persisted); err != nil {
|
|
t.Fatalf("json.Unmarshal(profiles.json) error = %v", err)
|
|
}
|
|
return persisted.Version
|
|
}
|
|
|
|
func readRawProfilesForTest(t *testing.T, configDir string) []byte {
|
|
t.Helper()
|
|
raw, err := os.ReadFile(ProfilesPath(configDir))
|
|
if err != nil {
|
|
t.Fatalf("ReadFile(profiles.json) error = %v", err)
|
|
}
|
|
return raw
|
|
}
|