Remove pre-release HTTP compatibility surface
This commit is contained in:
parent
15eb0038d2
commit
f28810ea33
1 changed files with 104 additions and 329 deletions
108
test/Spec.hs
108
test/Spec.hs
|
|
@ -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<script>" `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 =
|
||||
|
|
|
|||
Loading…
Reference in a new issue