Ctrl K
ComunidadGuías…

tests/android-onboarding-progress.unit.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import type { AndroidOnboardingProgress } from '../cli/src/build/onboarding/android/types.ts'
import { describe, expect, it } from 'vitest'
import { getAndroidResumeStep } from '../cli/src/build/onboarding/android/progress.ts'

// ─── Test fixture helpers ───────────────────────────────────────────
//
// Built compositionally so each test makes the minimum-relevant
// progress shape and the failure-localization on assertions stays
// clear: if `withFullKeystore(emptyProgress())` resumes to the wrong
// step, the offending field is unambiguous.

function emptyProgress(): AndroidOnboardingProgress {
  return {
    platform: 'android',
    appId: 'com.test.app',
    startedAt: '2026-05-21T00:00:00.000Z',
    completedSteps: {},
  }
}

function withFullKeystore(p: AndroidOnboardingProgress): AndroidOnboardingProgress {
  // `keystoreFullyValid` requires the marker AND all three ephemeral
  // fields. Helper sets them all so resume always passes the keystore
  // gate and tests focus on Phase 2+.
  return {
    ...p,
    keystoreAlias: 'release',
    keystoreStorePassword: 'pw',
    _keystoreBase64: 'base64-data',
    completedSteps: {
      ...p.completedSteps,
      keystoreReady: {
        keystorePath: 'release.p12',
        alias: 'release',
        isGenerated: true,
      },
    },
  }
}

function withGoogleSignIn(p: AndroidOnboardingProgress): AndroidOnboardingProgress {
  return {
    ...p,
    _oauthRefreshToken: 'refresh-token',
    completedSteps: {
      ...p.completedSteps,
      googleSignInComplete: {
        email: 'user@example.com',
        googleSubject: 'subject-123',
        scope: 'androidpublisher cloud-platform',
      },
    },
  }
}

function withPackageChosen(p: AndroidOnboardingProgress): AndroidOnboardingProgress {
  return {
    ...p,
    completedSteps: {
      ...p.completedSteps,
      androidPackageChosen: {
        packageName: 'com.test.app',
        source: 'gradle',
      },
    },
  }
}

// ─── Base routing ───────────────────────────────────────────────────

describe('getAndroidResumeStep — base routing', () => {
  it('returns welcome for null progress', () => {
    expect(getAndroidResumeStep(null)).toBe('welcome')
  })

  it('returns keystore-method-select when keystore is not started', () => {
    expect(getAndroidResumeStep(emptyProgress())).toBe('keystore-method-select')
  })
})

// ─── Legacy progress (backward compatibility contract) ──────────────
//
// Progress files created before the service-account fork existed have
// no `serviceAccountMethod` field. The compatibility contract: route
// them onto the existing OAuth path so in-flight onboardings continue
// where they were. Don't drop legacy users into the new fork.

describe('getAndroidResumeStep — legacy progress (no serviceAccountMethod)', () => {
  it('legacy progress with only keystore done routes to google-sign-in', () => {
    // No serviceAccountMethod → fall through to the OAuth-path rules.
    // !googleSignInComplete → google-sign-in. This is the contract.
    const p = withFullKeystore(emptyProgress())
    expect(getAndroidResumeStep(p)).toBe('google-sign-in')
  })

  it('legacy progress past google-sign-in continues to play-developer-id-input', () => {
    const p = withGoogleSignIn(withFullKeystore(emptyProgress()))
    expect(getAndroidResumeStep(p)).toBe('play-developer-id-input')
  })

  it('re-runs google-sign-in if the marker exists but the refresh token is missing', () => {
    // Defensive: a partial completion that lost the refresh token can't
    // mint new access tokens, so the rest of the OAuth chain would fail.
    // Treat it as never-signed-in.
    const p = withFullKeystore(emptyProgress())
    p.completedSteps.googleSignInComplete = {
      email: 'user@example.com',
      googleSubject: 'subject-123',
      scope: 'androidpublisher cloud-platform',
    }
    // Note: no _oauthRefreshToken
    expect(getAndroidResumeStep(p)).toBe('google-sign-in')
  })
})

// ─── Import path (serviceAccountMethod === 'existing') ──────────────

describe('getAndroidResumeStep — import path', () => {
  it('routes to android-package-select first if no package chosen yet', () => {
    // Package name is needed for the edits.insert validation probe.
    // Until we have it, the import path can't proceed past file pick.
    const p = withFullKeystore(emptyProgress())
    p.serviceAccountMethod = 'existing'
    expect(getAndroidResumeStep(p)).toBe('android-package-select')
  })

  it('routes to sa-json-existing-path after package chosen but no file picked', () => {
    const p = withPackageChosen(withFullKeystore(emptyProgress()))
    p.serviceAccountMethod = 'existing'
    expect(getAndroidResumeStep(p)).toBe('sa-json-existing-path')
  })

  it('routes to sa-json-validating after file picked but not yet accepted', () => {
    const p = withPackageChosen(withFullKeystore(emptyProgress()))
    p.serviceAccountMethod = 'existing'
    p.serviceAccountJsonPath = '/path/to/sa.json'
    expect(getAndroidResumeStep(p)).toBe('sa-json-validating')
  })

  it('routes to saving-credentials once the SA key bytes are accepted', () => {
    // `_serviceAccountKeyBase64` is set by EITHER a successful validation
    // OR the user picking "save anyway" on the failure recovery screen.
    // Both should converge to saving-credentials so we don't re-run
    // validation pointlessly on resume.
    const p = withPackageChosen(withFullKeystore(emptyProgress()))
    p.serviceAccountMethod = 'existing'
    p.serviceAccountJsonPath = '/path/to/sa.json'
    p._serviceAccountKeyBase64 = 'base64-sa-bytes'
    expect(getAndroidResumeStep(p)).toBe('saving-credentials')
  })

  it('honors save-anyway resume even with serviceAccountValidationSkipped=true', () => {
    // The skipped flag drives a banner log at save time; it must NOT
    // affect routing. The key bytes are the source of truth.
    const p = withPackageChosen(withFullKeystore(emptyProgress()))
    p.serviceAccountMethod = 'existing'
    p.serviceAccountJsonPath = '/path/to/sa.json'
    p._serviceAccountKeyBase64 = 'base64-sa-bytes'
    p.serviceAccountValidationSkipped = true
    expect(getAndroidResumeStep(p)).toBe('saving-credentials')
  })
})

// ─── Generate path (serviceAccountMethod === 'generate') ────────────

describe('getAndroidResumeStep — generate path', () => {
  it("'generate' explicitly set falls through to the OAuth-path rules", () => {
    const p = withFullKeystore(emptyProgress())
    p.serviceAccountMethod = 'generate'
    // Same as the legacy case but with an explicit marker — should still
    // land on google-sign-in because no OAuth steps are done.
    expect(getAndroidResumeStep(p)).toBe('google-sign-in')
  })

  it("'generate' continues to play-developer-id-input after sign-in", () => {
    const p = withGoogleSignIn(withFullKeystore(emptyProgress()))
    p.serviceAccountMethod = 'generate'
    expect(getAndroidResumeStep(p)).toBe('play-developer-id-input')
  })
})