Merge branch 'main' into codex/sync-wukong-capabilities-20260723
This commit is contained in:
@@ -0,0 +1,574 @@
|
||||
package smoke_test
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/keychain"
|
||||
)
|
||||
|
||||
// TestCLISmoke_LegacyAuthSlotsMigrateThroughStatus exercises the upgrade
|
||||
// boundary through a real CLI subprocess. Fixtures are deliberately written
|
||||
// as version-1 JSON and raw historical keychain accounts; using the current
|
||||
// auth SaveTokenData API here would only prove that the new format can read
|
||||
// itself.
|
||||
func TestCLISmoke_LegacyAuthSlotsMigrateThroughStatus(t *testing.T) {
|
||||
t.Run("v1.0.51 global-only slot", func(t *testing.T) {
|
||||
env, configDir := isolatedLegacyAuthCLIEnv(t)
|
||||
corpID, userID := legacyFixtureIdentity(t, "global")
|
||||
accessToken := "fixture-access-global"
|
||||
|
||||
writeLegacyProfilesV1(t, configDir, []legacySmokeProfile{{
|
||||
Name: "Legacy Global Org", CorpID: corpID, CorpName: "Legacy Global Org", UserID: userID,
|
||||
}}, corpID)
|
||||
seedRawLegacyKeychainSlot(t, keychain.AccountToken, legacyRawToken(
|
||||
t, accessToken, corpID, "Legacy Global Org", "", "",
|
||||
))
|
||||
|
||||
status := runLegacyAuthStatus(t, env)
|
||||
assertAuthenticatedStatus(t, status, corpID, userID)
|
||||
assertRawIdentitySlot(t, corpID, userID, accessToken)
|
||||
assertMigratedProfiles(t, configDir, map[string]string{corpID: userID})
|
||||
})
|
||||
|
||||
t.Run("v1.0.52 migrates every organization", func(t *testing.T) {
|
||||
env, configDir := isolatedLegacyAuthCLIEnv(t)
|
||||
profiles := make([]legacySmokeProfile, 0, 3)
|
||||
wantIdentities := make(map[string]string, 3)
|
||||
wantAccess := make(map[string]string, 3)
|
||||
for i := 0; i < 3; i++ {
|
||||
corpID, userID := legacyFixtureIdentity(t, fmt.Sprintf("org-%d", i))
|
||||
corpName := fmt.Sprintf("Legacy Multi Org %d", i+1)
|
||||
accessToken := fmt.Sprintf("fixture-access-multi-%d", i+1)
|
||||
profiles = append(profiles, legacySmokeProfile{
|
||||
Name: corpName, CorpID: corpID, CorpName: corpName, UserID: userID,
|
||||
})
|
||||
wantIdentities[corpID] = userID
|
||||
wantAccess[corpID] = accessToken
|
||||
// v1.0.52 kept one token per organization. The token response could
|
||||
// omit userId even when the v1 profile registry still knew it.
|
||||
seedRawLegacyKeychainSlot(t, legacyOrganizationAccount(corpID), legacyRawToken(
|
||||
t, accessToken, corpID, corpName, "", "",
|
||||
))
|
||||
}
|
||||
current := profiles[1]
|
||||
writeLegacyProfilesV1(t, configDir, profiles, current.CorpID)
|
||||
// v1.0.52 also mirrored the selected organization in the global slot.
|
||||
seedRawLegacyKeychainSlot(t, keychain.AccountToken, legacyRawToken(
|
||||
t, wantAccess[current.CorpID], current.CorpID, current.CorpName, "", "",
|
||||
))
|
||||
|
||||
status := runLegacyAuthStatus(t, env)
|
||||
assertAuthenticatedStatus(t, status, current.CorpID, current.UserID)
|
||||
for corpID, userID := range wantIdentities {
|
||||
assertRawIdentitySlot(t, corpID, userID, wantAccess[corpID])
|
||||
}
|
||||
assertMigratedProfiles(t, configDir, wantIdentities)
|
||||
})
|
||||
|
||||
t.Run("v1.0.52 unresolved external account remains usable", func(t *testing.T) {
|
||||
env, configDir := isolatedLegacyAuthCLIEnv(t)
|
||||
corpID, _ := legacyFixtureIdentity(t, "external")
|
||||
accessToken := "fixture-access-external"
|
||||
writeLegacyProfilesV1(t, configDir, []legacySmokeProfile{{
|
||||
Name: "Legacy External Org", CorpID: corpID, CorpName: "Legacy External Org",
|
||||
}}, corpID)
|
||||
seedRawLegacyKeychainSlot(t, legacyOrganizationAccount(corpID), legacyRawToken(
|
||||
t, accessToken, corpID, "Legacy External Org", "", "",
|
||||
))
|
||||
seedRawLegacyKeychainSlot(t, keychain.AccountToken, legacyRawToken(
|
||||
t, accessToken, corpID, "Legacy External Org", "", "",
|
||||
))
|
||||
|
||||
status := runLegacyAuthStatus(t, env)
|
||||
assertAuthenticatedStatus(t, status, corpID, "")
|
||||
assertRawOrganizationSlot(t, corpID, accessToken, "")
|
||||
|
||||
var migrated legacyProfilesDocument
|
||||
readJSONFile(t, filepath.Join(configDir, "profiles.json"), &migrated)
|
||||
if migrated.Version != 2 || len(migrated.Profiles) != 1 {
|
||||
t.Fatalf("migrated unresolved profiles = %#v", migrated)
|
||||
}
|
||||
if migrated.Profiles[0].CorpID != corpID || migrated.Profiles[0].UserID != "" {
|
||||
t.Fatalf("unresolved external identity was changed or discarded: %#v", migrated.Profiles[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("v1.0.53 v2 exact profile repairs global-only slot", func(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
globalTokenUserID bool
|
||||
}{
|
||||
{name: "global token omits user id"},
|
||||
{name: "global token user id matches", globalTokenUserID: true},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
env, configDir := isolatedLegacyAuthCLIEnv(t)
|
||||
corpID, userID := legacyFixtureIdentity(t, "v2-exact-"+tc.name)
|
||||
profile := legacySmokeProfile{
|
||||
Name: "Half Migrated Exact Account", CorpID: corpID,
|
||||
CorpName: "Half Migrated Exact Org", UserID: userID,
|
||||
}
|
||||
accessToken := "fixture-access-v2-exact-" + strings.ReplaceAll(tc.name, " ", "-")
|
||||
writeHalfMigratedProfilesV2(t, configDir, profile)
|
||||
|
||||
tokenUserID := ""
|
||||
if tc.globalTokenUserID {
|
||||
tokenUserID = userID
|
||||
}
|
||||
// Reproduce the interrupted 1.0.53 migration exactly: the registry is
|
||||
// already v2, while token material still exists only in the global
|
||||
// compatibility account. Do not seed either the org or identity slot.
|
||||
seedRawLegacyKeychainSlot(t, keychain.AccountToken, legacyRawToken(
|
||||
t, accessToken, corpID, profile.CorpName, tokenUserID, "",
|
||||
))
|
||||
|
||||
exactSelector := corpID + ":" + userID
|
||||
status := runLegacyAuthStatus(t, env, exactSelector)
|
||||
assertAuthenticatedStatus(t, status, corpID, userID)
|
||||
assertRawIdentitySlot(t, corpID, userID, accessToken)
|
||||
assertRawOrganizationSlot(t, corpID, accessToken, tokenUserID)
|
||||
assertMigratedProfiles(t, configDir, map[string]string{corpID: userID})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("v1.0.53 v2 unresolved profile repairs global-only organization slot", func(t *testing.T) {
|
||||
env, configDir := isolatedLegacyAuthCLIEnv(t)
|
||||
corpID, _ := legacyFixtureIdentity(t, "v2-unresolved")
|
||||
profile := legacySmokeProfile{
|
||||
Name: "Half Migrated External Account", CorpID: corpID,
|
||||
CorpName: "Half Migrated External Org",
|
||||
}
|
||||
accessToken := "fixture-access-v2-unresolved"
|
||||
writeHalfMigratedProfilesV2(t, configDir, profile)
|
||||
seedRawLegacyKeychainSlot(t, keychain.AccountToken, legacyRawToken(
|
||||
t, accessToken, corpID, profile.CorpName, "", "",
|
||||
))
|
||||
|
||||
status := runLegacyAuthStatus(t, env, profile.Name)
|
||||
assertAuthenticatedStatus(t, status, corpID, "")
|
||||
assertRawOrganizationSlot(t, corpID, accessToken, "")
|
||||
|
||||
var migrated legacyProfilesDocument
|
||||
readJSONFile(t, filepath.Join(configDir, "profiles.json"), &migrated)
|
||||
if migrated.Version != 2 || len(migrated.Profiles) != 1 {
|
||||
t.Fatalf("repaired unresolved v2 profiles = %#v", migrated)
|
||||
}
|
||||
if migrated.Profiles[0].CorpID != corpID || migrated.Profiles[0].UserID != "" {
|
||||
t.Fatalf("unresolved v2 identity was changed or discarded: %#v", migrated.Profiles[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestCLISmoke_ReservedUnresolvedAndExactProfilesStayIsolated protects the
|
||||
// schema-v3 selector boundary through real CLI subprocesses. A blank external
|
||||
// identity and a resolved account may legitimately coexist in one
|
||||
// organization; selecting either must never read or rewrite the other's slot.
|
||||
func TestCLISmoke_ReservedUnresolvedAndExactProfilesStayIsolated(t *testing.T) {
|
||||
env, configDir := isolatedLegacyAuthCLIEnv(t)
|
||||
corpID, exactUserID := legacyFixtureIdentity(t, "schema-v3-blank-exact")
|
||||
reservedSelector := "@legacy/" + base64.RawURLEncoding.EncodeToString([]byte(corpID))
|
||||
exactSelector := corpID + ":" + exactUserID
|
||||
blankAccessToken := "fixture-access-schema-v3-blank"
|
||||
exactAccessToken := "fixture-access-schema-v3-exact"
|
||||
blankRawToken := legacyRawToken(
|
||||
t, blankAccessToken, corpID, "Schema V3 Fixture Org", "", "",
|
||||
)
|
||||
exactRawToken := legacyRawToken(
|
||||
t, exactAccessToken, corpID, "Schema V3 Fixture Org", exactUserID, "Exact Fixture Account",
|
||||
)
|
||||
|
||||
writeCoexistingProfilesV3(t, configDir, corpID, exactUserID, reservedSelector, exactSelector)
|
||||
organizationAccount := legacyOrganizationAccount(corpID)
|
||||
identityAccount := legacyIdentityAccount(corpID, exactUserID)
|
||||
seedRawLegacyKeychainSlot(t, organizationAccount, blankRawToken)
|
||||
seedRawLegacyKeychainSlot(t, identityAccount, exactRawToken)
|
||||
|
||||
defaultStatus := runLegacyAuthStatus(t, env)
|
||||
assertAuthenticatedStatus(t, defaultStatus, corpID, "")
|
||||
exactStatus := runLegacyAuthStatus(t, env, exactSelector)
|
||||
assertAuthenticatedStatus(t, exactStatus, corpID, exactUserID)
|
||||
|
||||
assertRawKeychainSlotUnchanged(t, organizationAccount, blankRawToken)
|
||||
assertRawKeychainSlotUnchanged(t, identityAccount, exactRawToken)
|
||||
|
||||
var profiles legacyProfilesDocument
|
||||
readJSONFile(t, filepath.Join(configDir, "profiles.json"), &profiles)
|
||||
if profiles.Version != 3 {
|
||||
t.Fatalf("profiles version = %d, want 3", profiles.Version)
|
||||
}
|
||||
if profiles.CurrentProfile != reservedSelector {
|
||||
t.Fatalf("current profile = %q, want reserved blank selector %q", profiles.CurrentProfile, reservedSelector)
|
||||
}
|
||||
if profiles.OrgCurrentProfiles[corpID] != exactSelector {
|
||||
t.Fatalf(
|
||||
"orgCurrentProfiles[%q] = %q, want exact selector %q",
|
||||
corpID,
|
||||
profiles.OrgCurrentProfiles[corpID],
|
||||
exactSelector,
|
||||
)
|
||||
}
|
||||
if len(profiles.Profiles) != 2 {
|
||||
t.Fatalf("profiles = %#v, want one blank and one exact identity", profiles.Profiles)
|
||||
}
|
||||
identities := map[string]int{}
|
||||
for _, profile := range profiles.Profiles {
|
||||
if profile.CorpID != corpID {
|
||||
t.Fatalf("profile corpId = %q, want %q", profile.CorpID, corpID)
|
||||
}
|
||||
identities[profile.UserID]++
|
||||
}
|
||||
if identities[""] != 1 || identities[exactUserID] != 1 {
|
||||
t.Fatalf("profile identities = %#v, want one blank and one exact %q", identities, exactUserID)
|
||||
}
|
||||
}
|
||||
|
||||
type legacySmokeProfile struct {
|
||||
Name string
|
||||
CorpID string
|
||||
CorpName string
|
||||
UserID string
|
||||
UserName string
|
||||
}
|
||||
|
||||
type legacyProfilesDocument struct {
|
||||
Version int `json:"version"`
|
||||
CurrentProfile string `json:"currentProfile"`
|
||||
OrgCurrentProfiles map[string]string `json:"orgCurrentProfiles"`
|
||||
Profiles []struct {
|
||||
CorpID string `json:"corpId"`
|
||||
UserID string `json:"userId"`
|
||||
} `json:"profiles"`
|
||||
}
|
||||
|
||||
type legacyAuthStatus struct {
|
||||
Success bool `json:"success"`
|
||||
Authenticated bool `json:"authenticated"`
|
||||
Refreshed bool `json:"refreshed"`
|
||||
TokenValid bool `json:"token_valid"`
|
||||
CorpID string `json:"corp_id"`
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
func isolatedLegacyAuthCLIEnv(t *testing.T) ([]string, string) {
|
||||
t.Helper()
|
||||
env := isolatedCLIEnv(t)
|
||||
configDir := smokeEnvValue(t, env, "DWS_CONFIG_DIR")
|
||||
// File-backed platforms are already isolated by DWS_KEYCHAIN_DIR. Windows
|
||||
// uses HKCU, so give the parent seeder and CLI child the same throwaway
|
||||
// registry namespace as well.
|
||||
namespace := filepath.Join(configDir, "legacy-auth-smoke")
|
||||
t.Setenv(keychain.TestNamespaceEnv, namespace)
|
||||
env = append(env, keychain.TestNamespaceEnv+"="+namespace)
|
||||
sort.Strings(env)
|
||||
|
||||
if err := keychain.RemoveAuthTokenEntries(keychain.Service); err != nil {
|
||||
t.Fatalf("clear isolated historical keychain: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = keychain.RemoveAuthTokenEntries(keychain.Service)
|
||||
})
|
||||
return env, configDir
|
||||
}
|
||||
|
||||
func smokeEnvValue(t *testing.T, env []string, key string) string {
|
||||
t.Helper()
|
||||
prefix := key + "="
|
||||
for _, entry := range env {
|
||||
if strings.HasPrefix(entry, prefix) {
|
||||
return strings.TrimPrefix(entry, prefix)
|
||||
}
|
||||
}
|
||||
t.Fatalf("CLI environment does not contain %s", key)
|
||||
return ""
|
||||
}
|
||||
|
||||
func legacyFixtureIdentity(t *testing.T, suffix string) (string, string) {
|
||||
t.Helper()
|
||||
sum := sha256.Sum256([]byte(t.Name() + "\x00" + suffix))
|
||||
return fmt.Sprintf("ding_fixture_%x", sum[:6]), fmt.Sprintf("fixture-user-%x", sum[6:12])
|
||||
}
|
||||
|
||||
func writeLegacyProfilesV1(t *testing.T, configDir string, profiles []legacySmokeProfile, currentCorpID string) {
|
||||
t.Helper()
|
||||
rawProfiles := make([]map[string]any, 0, len(profiles))
|
||||
for _, profile := range profiles {
|
||||
raw := map[string]any{
|
||||
"name": profile.Name,
|
||||
"corpId": profile.CorpID,
|
||||
"corpName": profile.CorpName,
|
||||
"clientId": "fixture-legacy-client",
|
||||
"status": "active",
|
||||
"expiresAt": "2099-01-02T03:04:05Z",
|
||||
}
|
||||
if profile.UserID != "" {
|
||||
raw["userId"] = profile.UserID
|
||||
}
|
||||
if profile.UserName != "" {
|
||||
raw["userName"] = profile.UserName
|
||||
}
|
||||
rawProfiles = append(rawProfiles, raw)
|
||||
}
|
||||
document := map[string]any{
|
||||
"version": 1,
|
||||
"primaryProfile": profiles[0].CorpID,
|
||||
"currentProfile": currentCorpID,
|
||||
"profiles": rawProfiles,
|
||||
}
|
||||
data, err := json.MarshalIndent(document, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("marshal raw v1 profiles fixture: %v", err)
|
||||
}
|
||||
if err := os.MkdirAll(configDir, 0o700); err != nil {
|
||||
t.Fatalf("create raw v1 config directory: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(configDir, "profiles.json"), append(data, '\n'), 0o600); err != nil {
|
||||
t.Fatalf("write raw v1 profiles fixture: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func writeHalfMigratedProfilesV2(t *testing.T, configDir string, profile legacySmokeProfile) {
|
||||
t.Helper()
|
||||
selector := profile.CorpID
|
||||
orgCurrentProfiles := map[string]string{}
|
||||
if profile.UserID != "" {
|
||||
selector = profile.CorpID + ":" + profile.UserID
|
||||
orgCurrentProfiles[profile.CorpID] = selector
|
||||
}
|
||||
rawProfile := map[string]any{
|
||||
"name": profile.Name,
|
||||
"corpId": profile.CorpID,
|
||||
"corpName": profile.CorpName,
|
||||
"clientId": "fixture-legacy-client",
|
||||
"status": "active",
|
||||
"expiresAt": "2099-01-02T03:04:05Z",
|
||||
}
|
||||
if profile.UserID != "" {
|
||||
rawProfile["userId"] = profile.UserID
|
||||
}
|
||||
document := map[string]any{
|
||||
"version": 2,
|
||||
"primaryProfile": selector,
|
||||
"currentProfile": selector,
|
||||
"orgCurrentProfiles": orgCurrentProfiles,
|
||||
"profiles": []map[string]any{rawProfile},
|
||||
}
|
||||
data, err := json.MarshalIndent(document, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("marshal raw half-migrated v2 profiles fixture: %v", err)
|
||||
}
|
||||
if err := os.MkdirAll(configDir, 0o700); err != nil {
|
||||
t.Fatalf("create raw v2 config directory: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(configDir, "profiles.json"), append(data, '\n'), 0o600); err != nil {
|
||||
t.Fatalf("write raw half-migrated v2 profiles fixture: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func writeCoexistingProfilesV3(
|
||||
t *testing.T,
|
||||
configDir string,
|
||||
corpID string,
|
||||
exactUserID string,
|
||||
reservedSelector string,
|
||||
exactSelector string,
|
||||
) {
|
||||
t.Helper()
|
||||
document := map[string]any{
|
||||
"version": 3,
|
||||
"primaryProfile": exactSelector,
|
||||
"currentProfile": reservedSelector,
|
||||
"previousProfile": exactSelector,
|
||||
"orgCurrentProfiles": map[string]string{corpID: exactSelector},
|
||||
"profiles": []map[string]any{
|
||||
{
|
||||
"name": "Schema V3 Fixture Org",
|
||||
"corpId": corpID,
|
||||
"corpName": "Schema V3 Fixture Org",
|
||||
"clientId": "fixture-legacy-client",
|
||||
"status": "active",
|
||||
"expiresAt": "2099-01-02T03:04:05Z",
|
||||
},
|
||||
{
|
||||
"name": "Schema V3 Exact Fixture",
|
||||
"corpId": corpID,
|
||||
"corpName": "Schema V3 Fixture Org",
|
||||
"userId": exactUserID,
|
||||
"userName": "Exact Fixture Account",
|
||||
"clientId": "fixture-legacy-client",
|
||||
"status": "active",
|
||||
"expiresAt": "2099-01-02T03:04:05Z",
|
||||
},
|
||||
},
|
||||
}
|
||||
data, err := json.MarshalIndent(document, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("marshal raw coexisting v3 profiles fixture: %v", err)
|
||||
}
|
||||
if err := os.MkdirAll(configDir, 0o700); err != nil {
|
||||
t.Fatalf("create raw v3 config directory: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(configDir, "profiles.json"), append(data, '\n'), 0o600); err != nil {
|
||||
t.Fatalf("write raw coexisting v3 profiles fixture: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func legacyRawToken(t *testing.T, accessToken, corpID, corpName, userID, userName string) string {
|
||||
t.Helper()
|
||||
document := map[string]any{
|
||||
"access_token": accessToken,
|
||||
"refresh_token": "fixture-refresh-" + accessToken,
|
||||
"persistent_code": "fixture-persistent-" + accessToken,
|
||||
"expires_at": "2099-01-02T03:04:05Z",
|
||||
"refresh_expires_at": "2099-02-02T03:04:05Z",
|
||||
"corp_id": corpID,
|
||||
"corp_name": corpName,
|
||||
"client_id": "fixture-legacy-client",
|
||||
"source": "mcp",
|
||||
}
|
||||
if userID != "" {
|
||||
document["user_id"] = userID
|
||||
}
|
||||
if userName != "" {
|
||||
document["user_name"] = userName
|
||||
}
|
||||
data, err := json.MarshalIndent(document, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("marshal raw historical token: %v", err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func seedRawLegacyKeychainSlot(t *testing.T, account, rawToken string) {
|
||||
t.Helper()
|
||||
// Write the old account directly. Do not route setup through auth.SaveTokenData,
|
||||
// which would eagerly create the very identity slot this E2E must observe.
|
||||
if err := keychain.Set(keychain.Service, account, rawToken); err != nil {
|
||||
t.Fatalf("write raw historical keychain account %q: %v", account, err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertRawKeychainSlotUnchanged(t *testing.T, account, want string) {
|
||||
t.Helper()
|
||||
got, err := keychain.Get(keychain.Service, account)
|
||||
if err != nil {
|
||||
t.Fatalf("read raw keychain account %q: %v", account, err)
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("raw keychain account %q changed\nwant:\n%s\ngot:\n%s", account, want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func runLegacyAuthStatus(t *testing.T, env []string, profile ...string) legacyAuthStatus {
|
||||
t.Helper()
|
||||
if len(profile) > 1 {
|
||||
t.Fatalf("auth status accepts at most one profile selector, got %d", len(profile))
|
||||
}
|
||||
args := []string{"--format", "json", "auth", "status"}
|
||||
if len(profile) == 1 && strings.TrimSpace(profile[0]) != "" {
|
||||
args = append(args, "--profile", profile[0])
|
||||
}
|
||||
stdout, stderr, err := runCLI(t, env, args...)
|
||||
if err != nil {
|
||||
t.Fatalf("dws auth status failed: %v\nstdout:\n%s\nstderr:\n%s", err, stdout, stderr)
|
||||
}
|
||||
var status legacyAuthStatus
|
||||
if err := json.Unmarshal([]byte(stdout), &status); err != nil {
|
||||
t.Fatalf("dws auth status returned invalid JSON: %v\nstdout:\n%s\nstderr:\n%s", err, stdout, stderr)
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
func assertAuthenticatedStatus(t *testing.T, status legacyAuthStatus, corpID, userID string) {
|
||||
t.Helper()
|
||||
if !status.Success || !status.Authenticated || !status.TokenValid || status.Refreshed {
|
||||
t.Fatalf("auth status did not reuse valid historical login: %#v", status)
|
||||
}
|
||||
if status.CorpID != corpID || status.UserID != userID {
|
||||
t.Fatalf("auth status identity = %q:%q, want %q:%q", status.CorpID, status.UserID, corpID, userID)
|
||||
}
|
||||
}
|
||||
|
||||
func assertRawIdentitySlot(t *testing.T, corpID, userID, accessToken string) {
|
||||
t.Helper()
|
||||
account := legacyIdentityAccount(corpID, userID)
|
||||
raw, err := keychain.Get(keychain.Service, account)
|
||||
if err != nil {
|
||||
t.Fatalf("read migrated identity account %q: %v", account, err)
|
||||
}
|
||||
if raw == "" {
|
||||
t.Fatalf("migrated identity account %q is missing", account)
|
||||
}
|
||||
var token map[string]any
|
||||
if err := json.Unmarshal([]byte(raw), &token); err != nil {
|
||||
t.Fatalf("parse migrated identity account %q: %v", account, err)
|
||||
}
|
||||
if token["access_token"] != accessToken || token["corp_id"] != corpID || token["user_id"] != userID {
|
||||
t.Fatalf("migrated identity account %q = %#v", account, token)
|
||||
}
|
||||
}
|
||||
|
||||
func assertRawOrganizationSlot(t *testing.T, corpID, accessToken, userID string) {
|
||||
t.Helper()
|
||||
account := legacyOrganizationAccount(corpID)
|
||||
raw, err := keychain.Get(keychain.Service, account)
|
||||
if err != nil {
|
||||
t.Fatalf("read organization account %q: %v", account, err)
|
||||
}
|
||||
var token map[string]any
|
||||
if err := json.Unmarshal([]byte(raw), &token); err != nil {
|
||||
t.Fatalf("parse organization account %q: %v", account, err)
|
||||
}
|
||||
if token["access_token"] != accessToken || token["corp_id"] != corpID {
|
||||
t.Fatalf("organization account %q = %#v", account, token)
|
||||
}
|
||||
if got, _ := token["user_id"].(string); got != userID {
|
||||
t.Fatalf("organization account %q user_id = %q, want %q", account, got, userID)
|
||||
}
|
||||
}
|
||||
|
||||
func assertMigratedProfiles(t *testing.T, configDir string, identities map[string]string) {
|
||||
t.Helper()
|
||||
var migrated legacyProfilesDocument
|
||||
readJSONFile(t, filepath.Join(configDir, "profiles.json"), &migrated)
|
||||
if migrated.Version != 2 {
|
||||
t.Fatalf("profiles version = %d, want 2", migrated.Version)
|
||||
}
|
||||
if len(migrated.Profiles) != len(identities) {
|
||||
t.Fatalf("migrated profile count = %d, want %d", len(migrated.Profiles), len(identities))
|
||||
}
|
||||
for corpID, userID := range identities {
|
||||
selector := corpID + ":" + userID
|
||||
if migrated.OrgCurrentProfiles[corpID] != selector {
|
||||
t.Errorf("orgCurrentProfiles[%q] = %q, want %q", corpID, migrated.OrgCurrentProfiles[corpID], selector)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readJSONFile(t *testing.T, path string, target any) {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", path, err)
|
||||
}
|
||||
if err := json.Unmarshal(data, target); err != nil {
|
||||
t.Fatalf("parse %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
func legacyOrganizationAccount(corpID string) string {
|
||||
return keychain.AccountToken + ":" + strings.TrimSpace(corpID)
|
||||
}
|
||||
|
||||
func legacyIdentityAccount(corpID, userID string) string {
|
||||
identity := strings.TrimSpace(corpID) + "\x00" + strings.TrimSpace(userID)
|
||||
return fmt.Sprintf("%s:id:%x", keychain.AccountToken, sha256.Sum256([]byte(identity)))
|
||||
}
|
||||
Reference in New Issue
Block a user