@@ -561,6 +561,25 @@ struct MiMoProviderTests {
561561
562562 #expect( elapsed < . seconds( 1 ) , " Required failure was delayed by optional requests: \( elapsed) " )
563563 }
564+
565+ @Test
566+ func `fetch usage treats auth redirect as login required `() async throws {
567+ let transport = ProviderHTTPTransportStub { request in
568+ let url = try #require( request. url)
569+ let ( response, data) = Self . makeResponse ( url: url, body: " " , statusCode: 302 )
570+ return ( data, response)
571+ }
572+
573+ do {
574+ _ = try await MiMoUsageFetcher . fetchUsage (
575+ cookieHeader: " userId=123; api-platform_serviceToken=expired-token " ,
576+ environment: [ " MIMO_API_URL " : " https://mimo.test/api/v1 " ] ,
577+ session: transport)
578+ Issue . record ( " Expected MiMo auth redirect to require login " )
579+ } catch MiMoUsageError . loginRequired {
580+ // Expected.
581+ }
582+ }
564583}
565584
566585private actor MiMoOptionalRequestGate {
@@ -942,6 +961,73 @@ extension MiMoProviderTests {
942961 #expect( CookieHeaderCache . load ( provider: . mimo) ? . sourceLabel == " Active Chrome " )
943962 }
944963
964+ @Test
965+ func `mimo web strategy retries safari after stale chrome auth redirect`() async throws {
966+ KeychainCacheStore . setTestStoreForTesting ( true )
967+ defer { KeychainCacheStore . setTestStoreForTesting ( false ) }
968+ let registered = URLProtocol . registerClass ( MiMoStubURLProtocol . self)
969+ defer {
970+ if registered {
971+ URLProtocol . unregisterClass ( MiMoStubURLProtocol . self)
972+ }
973+ MiMoStubURLProtocol . handler = nil
974+ MiMoCookieImporter . importSessionsOverrideForTesting = nil
975+ CookieHeaderCache . clear ( provider: . mimo)
976+ }
977+
978+ CookieHeaderCache . clear ( provider: . mimo)
979+ CookieHeaderCache . store (
980+ provider: . mimo,
981+ cookieHeader: " api-platform_serviceToken=stale-chrome-token; userId=111 " ,
982+ sourceLabel: " Chrome " )
983+
984+ MiMoCookieImporter . importSessionsOverrideForTesting = { _, _ in
985+ [
986+ . init(
987+ cookieHeader: " api-platform_serviceToken=stale-chrome-token; userId=111 " ,
988+ sourceLabel: " Chrome " ) ,
989+ . init(
990+ cookieHeader: " api-platform_serviceToken=valid-safari-token; userId=222 " ,
991+ sourceLabel: " Safari " ) ,
992+ ]
993+ }
994+
995+ let lock = NSLock ( )
996+ var requestedCookies : [ String ] = [ ]
997+ MiMoStubURLProtocol . handler = { request in
998+ guard let url = request. url else { throw URLError ( . badURL) }
999+ let cookie = request. value ( forHTTPHeaderField: " Cookie " ) ?? " "
1000+ lock. withLock {
1001+ requestedCookies. append ( cookie)
1002+ }
1003+
1004+ if cookie. contains ( " stale-chrome-token " ) {
1005+ return Self . makeResponse ( url: url, body: " " , statusCode: 302 )
1006+ }
1007+
1008+ let body = """
1009+ {
1010+ " code " : 0,
1011+ " message " : " " ,
1012+ " data " : {
1013+ " balance " : " 25.51 " ,
1014+ " currency " : " USD "
1015+ }
1016+ }
1017+ """
1018+ return Self . makeResponse ( url: url, body: body)
1019+ }
1020+
1021+ let strategy = MiMoWebFetchStrategy ( )
1022+ let result = try await strategy
1023+ . fetch ( self . makeContext ( environment: [ " MIMO_API_URL " : " https://mimo.test/api/v1 " ] ) )
1024+
1025+ #expect( requestedCookies. contains ( where: { $0. contains ( " stale-chrome-token " ) } ) )
1026+ #expect( requestedCookies. contains ( where: { $0. contains ( " valid-safari-token " ) } ) )
1027+ #expect( result. usage. mimoUsage? . balanceDetail == " $25.51 " )
1028+ #expect( CookieHeaderCache . load ( provider: . mimo) ? . sourceLabel == " Safari " )
1029+ }
1030+
9451031 #if os(macOS)
9461032 @Test
9471033 func `mimo import er merges profile stores before validating auth cookies`() {
0 commit comments