Remove pre-release HTTP compatibility surface

This commit is contained in:
ms2k 2026-07-19 17:49:03 +09:00
commit f28810ea33

View file

@ -13,6 +13,7 @@ import Control.Exception
, throwIO
, try
)
import Control.Monad (forM_)
import Control.Monad.IO.Class (liftIO)
import Control.Retry (simulatePolicy)
import Control.Lens ((&), (?~))
@ -129,7 +130,6 @@ import Nuthatch.Streaming
, maximumMultipartBytes
, multipartPartBytes
)
import Nuthatch.Ui (browserScript, productPage)
import Servant
import Servant.Server.Experimental.Auth (AuthHandler, mkAuthHandler)
import Servant.Types.SourceT
@ -623,8 +623,8 @@ apiTests = testGroup "typed HTTP boundary"
("prefix_mutation_rejected" `BS.isInfixOf` BL.toStrict prefixError)
, testCase "production authentication validates cookies and callback emits secure cookies"
testAuthenticationBoundary
, testCase "legacy browser entry directs safely to the static application"
testBrowserBridge
, testCase "only canonical product routes are exposed"
testCanonicalProductRoutes
, testCase "links derive from the type-level API" $
show (linkURI (safeLink api (Proxy :: Proxy HealthEndpoint)))
@?= "healthz"
@ -648,7 +648,7 @@ testValidDecoding :: IO ()
testValidDecoding = do
calls <- newIORef 0
response <- apiRequest calls methodGet
"/?root=personal&prefix=docs%2Freports&q=quarterly&continuation=next"
"/api/browse?root=personal&prefix=docs%2Freports&q=quarterly&continuation=next"
[]
""
simpleStatus response @?= status200
@ -657,16 +657,17 @@ testValidDecoding = do
testInvalidDomains :: IO ()
testInvalidDomains = do
calls <- newIORef 0
invalidRoot <- apiRequest calls methodGet "/?root=PERSONAL" [] ""
invalidRoot <- apiRequest calls methodGet "/api/browse?root=PERSONAL" [] ""
simpleStatus invalidRoot @?= status400
invalidPath <- apiRequest calls methodGet "/?prefix=docs%2F..%2Fprivate" [] ""
invalidPath <- apiRequest calls methodGet
"/api/browse?prefix=docs%2F..%2Fprivate" [] ""
simpleStatus invalidPath @?= status400
readIORef calls >>= (@?= 0)
testMalformedUtf8 :: IO ()
testMalformedUtf8 = do
calls <- newIORef 0
response <- apiRequest calls methodGet "/?q=%FF" [] ""
response <- apiRequest calls methodGet "/api/browse?q=%FF" [] ""
simpleStatus response @?= status400
readIORef calls >>= (@?= 0)
@ -681,16 +682,17 @@ testUploadDecoding = do
, ("X-CSRF-Token", csrf)
]
response <- apiRequest calls methodPost
"/upload?root=personal&path=docs%2Ffile.txt&operation_id=bad%20id"
"/api/upload?root=personal&path=docs%2Ffile.txt&operation_id=bad%20id"
headers
"body"
simpleStatus response @?= status400
invalidSize <- apiRequest calls methodPost
"/upload?root=personal&path=docs%2Ffile.txt&operation_id=upload-1"
"/api/upload?root=personal&path=docs%2Ffile.txt&operation_id=upload-1"
(replaceHeader "X-Declared-Size" "zero" headers)
"body"
simpleStatus invalidSize @?= status400
invalidPagination <- apiRequest calls methodGet "/?continuation=" [] ""
invalidPagination <- apiRequest calls methodGet
"/api/browse?continuation=" [] ""
simpleStatus invalidPagination @?= status400
readIORef calls >>= (@?= 0)
@ -705,7 +707,7 @@ testStreamingEndpoints = do
, ("X-CSRF-Token", csrf)
]
upload <- apiRequest calls methodPost
"/upload?root=personal&path=docs%2Ffile.txt&operation_id=upload-1"
"/api/upload?root=personal&path=docs%2Ffile.txt&operation_id=upload-1"
headers
"body"
simpleStatus upload @?= status200
@ -713,7 +715,7 @@ testStreamingEndpoints = do
("\"idempotency_window_seconds\":3600"
`BS.isInfixOf` BL.toStrict (simpleBody upload))
download <- apiRequest calls methodGet
"/download?root=shared&path=docs%2Ffile.txt"
"/api/download?root=shared&path=docs%2Ffile.txt"
[]
""
simpleStatus download @?= status200
@ -735,10 +737,10 @@ testOperationRepresentations = do
[("Cookie", "nuthatch_oidc=transaction")]
""
simpleStatus callback @?= status303
legacyLogin <- apiRequest calls methodPost "/login" [] ""
simpleStatus legacyLogin @?= status405
wrongLoginMethod <- apiRequest calls methodPost "/login" [] ""
simpleStatus wrongLoginMethod @?= status405
deletion <- apiRequest calls methodPost
"/delete"
"/api/delete"
[("Content-Type", "application/json")]
"{\"root\":\"shared\",\"path\":\"docs/file.txt\",\"operation_id\":\"delete-1\",\"csrf\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}"
simpleStatus deletion @?= status200
@ -748,7 +750,7 @@ testOperationRepresentations = do
("\"idempotency_window_seconds\":3600"
`BS.isInfixOf` BL.toStrict (simpleBody deletion))
malformed <- apiRequest calls methodPost
"/delete"
"/api/delete"
[("Content-Type", "application/json")]
"{\"root\":\"shared\",\"path\":\"../escape\"}"
simpleStatus malformed @?= status400
@ -815,7 +817,6 @@ typedTestServer :: IORef Int -> Server Api
typedTestServer calls =
counted (HealthResponse True)
:<|> counted (ReadyResponse True)
:<|> counted (BrowserScript "script")
:<|> (\_ -> counted (addHeader "default-src 'none'" (HtmlPage "app")))
:<|> (\_ -> Tagged $ \_ sendResponse ->
sendResponse (responseLBS status404 [] ""))
@ -831,29 +832,6 @@ typedTestServer calls =
(addHeader "session=expired"
(addHeader "csrf=expired"
(addHeader "transaction=expired" NoContent))))
:<|> (\_ _ _ _ _ -> counted
(BrowseView
(HtmlPage "browse")
BrowseResponse
{ browseEntries = []
, browseContinuation = Nothing
, browsePartial = False
}))
:<|> (\_ _ _ _ -> counted
(addHeader "attachment; filename=\"file.txt\""
(addHeader "nosniff"
(addHeader "sandbox" (addHeader 4 (source ["body"]))))))
:<|> (\_ _ _ operation _ _ _ _ -> counted UploadResponse
{ uploadResponseOperationId = operation
, uploadResponseResult = MutationSucceeded
, uploadResponseIdempotencyWindowSeconds = 3600
})
:<|> (\_ request -> counted DeleteResponse
{ deleteResponseOperationId = deleteOperationId request
, deleteResponseResult = MutationSucceeded
, deleteResponseIdempotencyWindowSeconds = 3600
})
:<|> (\_ _ -> counted (DeletePrefixResponse 1))
:<|>
( (\session -> counted SessionResponse
{ sessionResponseUserId = suUserId (sessionUser session)
@ -883,13 +861,11 @@ typedTestServer calls =
counted :: a -> Handler a
counted value = liftIO (modifyIORef' calls (+ 1)) >> pure value
emptyBrowse =
BrowseView
(HtmlPage "browse")
BrowseResponse
{ browseEntries = []
, browseContinuation = Nothing
, browsePartial = False
}
BrowseResponse
{ browseEntries = []
, browseContinuation = Nothing
, browsePartial = False
}
downloadResponse =
addHeader "attachment; filename=\"file.txt\"" $
addHeader "nosniff" $
@ -932,7 +908,7 @@ testAuthenticationBoundary = do
"nuthatch_session=" <> TE.encodeUtf8 (unSessionToken token)
<> "; nuthatch_csrf=" <> TE.encodeUtf8 (unCsrfToken csrf)
authenticated <- apiRequestWith app methodGet
"/?root=personal"
"/api/browse?root=personal"
[("Cookie", cookie)]
""
simpleStatus authenticated @?= status200
@ -940,7 +916,7 @@ testAuthenticationBoundary = do
writeIORef calls 0
malformed <- apiRequestWith app methodGet
"/?root=personal"
"/api/browse?root=personal"
[("Cookie", "nuthatch_session=" <> BS.singleton 255
<> "; nuthatch_csrf=" <> TE.encodeUtf8 (unCsrfToken csrf))]
""
@ -948,7 +924,7 @@ testAuthenticationBoundary = do
readIORef calls >>= (@?= 0)
malformedGrammar <- apiRequestWith app methodGet
"/?root=personal"
"/api/browse?root=personal"
[("Cookie", "nuthatch_session")]
""
simpleStatus malformedGrammar @?= status400
@ -961,33 +937,33 @@ testAuthenticationBoundary = do
| otherwise -> prefix <> "a"
Nothing -> "invalid"
tampered <- apiRequestWith app methodGet
"/?root=personal"
"/api/browse?root=personal"
[("Cookie", "nuthatch_session=" <> TE.encodeUtf8 tamperedToken)]
""
simpleStatus tampered @?= status303
readIORef calls >>= (@?= 0)
missingCsrf <- apiRequestWith app methodGet
"/?root=personal"
"/api/browse?root=personal"
[("Cookie", "nuthatch_session=" <> TE.encodeUtf8 (unSessionToken token))]
""
simpleStatus missingCsrf @?= status200
readIORef calls >>= (@?= 1)
testBrowserBridge :: IO ()
testBrowserBridge = do
let entry = ListEntry
{ leName = "folder"
, leIsPrefix = True
, leSize = Nothing
, leRelativePath = "folder<script>"
}
browse = productPage [entry]
assertBool "legacy page directs browsers to the static application"
("/app" `T.isInfixOf` browse && "/app" `T.isInfixOf` browserScript)
assertBool "legacy page escapes object paths"
("folder&lt;script&gt;" `T.isInfixOf` browse
&& not ("folder<script>" `T.isInfixOf` browse))
testCanonicalProductRoutes :: IO ()
testCanonicalProductRoutes = do
calls <- newIORef 0
forM_
[ (methodGet, "/")
, (methodGet, "/nuthatch.js")
, (methodGet, "/download?root=personal&path=file")
, (methodPost, "/upload")
, (methodPost, "/delete")
, (methodPost, "/delete-prefix")
] $ \(method, path) -> do
response <- apiRequest calls method path [] ""
simpleStatus response @?= status404
readIORef calls >>= (@?= 0)
productionAuthTestApplication :: Config -> IORef Int -> Application
productionAuthTestApplication cfg calls =