Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions obp-api/src/main/scala/code/api/cache/RedisLogger.scala
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,24 @@ object RedisLogger {
}.getOrElse(LogTail(Nil))
}

/**
* Read latest messages from Redis FIFO queue with pagination support.
*/
def getLogTail(level: LogLevel.LogLevel, limit: Option[Int], offset: Option[Int]): LogTail = {
val fullLogTail = getLogTail(level)
val entries = fullLogTail.entries

// Apply pagination
val paginatedEntries = (offset, limit) match {
case (Some(off), Some(lim)) => entries.drop(off).take(lim)
case (Some(off), None) => entries.drop(off)
case (None, Some(lim)) => entries.take(lim)
case (None, None) => entries
}

LogTail(paginatedEntries)
}

/**
* Get Redis logging statistics
*/
Expand Down
15 changes: 13 additions & 2 deletions obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,12 @@ trait APIMethods510 {
"""Returns information about:
|
|* Log Cache
|
|This endpoint supports pagination via the following optional query parameters:
|* limit - Maximum number of log entries to return
|* offset - Number of log entries to skip (for pagination)
|
|Example: GET /dev-ops/log-cache/INFO?limit=50&offset=100
""",
EmptyBody,
EmptyBody,
Expand All @@ -271,8 +277,13 @@ trait APIMethods510 {
roles = RedisLogger.LogLevel.requiredRoles(level),
callContext = cc.callContext
)
// Fetch logs
logs <- Future(RedisLogger.getLogTail(level))
httpParams <- NewStyle.function.extractHttpParamsFromUrl(cc.url)
(obpQueryParams, callContext) <- createQueriesByHttpParamsFuture(httpParams, cc.callContext)
// Extract limit and offset from query parameters
limit = obpQueryParams.collectFirst { case OBPLimit(value) => value }
offset = obpQueryParams.collectFirst { case OBPOffset(value) => value }
// Fetch logs with pagination
logs <- Future(RedisLogger.getLogTail(level, limit, offset))
} yield {
(logs, HttpCode.`200`(cc.callContext))
}
Expand Down
245 changes: 245 additions & 0 deletions obp-api/src/test/scala/code/api/v5_1_0/LogCacheEndpointTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
package code.api.v5_1_0

import code.api.util.APIUtil.OAuth._
import code.api.util.ApiRole.CanGetAllLevelLogsAtAllBanks
import code.api.util.ErrorMessages.{UserHasMissingRoles, UserNotLoggedIn}
import code.api.v5_1_0.OBPAPI5_1_0.Implementations5_1_0
import code.entitlement.Entitlement
import com.github.dwickern.macros.NameOf.nameOf
import com.openbankproject.commons.model.ErrorMessage
import com.openbankproject.commons.util.ApiVersion
import net.liftweb.json.JsonAST._
import org.scalatest.Tag

class LogCacheEndpointTest extends V510ServerSetup {

/**
* Test tags
* Example: To run tests with tag "logCacheEndpoint":
* mvn test -D tagsToInclude
*
* This is made possible by the scalatest maven plugin
*/
object VersionOfApi extends Tag(ApiVersion.v5_1_0.toString)
object ApiEndpoint1 extends Tag(nameOf(Implementations5_1_0.logCacheEndpoint))

feature(s"test $ApiEndpoint1 version $VersionOfApi - Unauthorized access") {
scenario("We will call the endpoint without user credentials", ApiEndpoint1, VersionOfApi) {
When("We make a request v5.1.0")
val request = (v5_1_0_Request / "dev-ops" / "log-cache" / "INFO").GET

Check failure on line 29 in obp-api/src/test/scala/code/api/v5_1_0/LogCacheEndpointTest.scala

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "dev-ops" 15 times.

See more on https://sonarcloud.io/project/issues?id=OpenBankProject_OBP-API&issues=AZq_Mo2hSchGjxeKzdXC&open=AZq_Mo2hSchGjxeKzdXC&pullRequest=2634

Check failure on line 29 in obp-api/src/test/scala/code/api/v5_1_0/LogCacheEndpointTest.scala

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "log-cache" 15 times.

See more on https://sonarcloud.io/project/issues?id=OpenBankProject_OBP-API&issues=AZq_Mo2hSchGjxeKzdXE&open=AZq_Mo2hSchGjxeKzdXE&pullRequest=2634
val response = makeGetRequest(request)
Then("We should get a 401")
response.code should equal(401)
response.body.extract[ErrorMessage].message should equal(UserNotLoggedIn)
}
}

feature(s"test $ApiEndpoint1 version $VersionOfApi - Missing entitlement") {
scenario("We will call the endpoint with user credentials but without proper entitlement", ApiEndpoint1, VersionOfApi) {
When("We make a request v5.1.0")
val request = (v5_1_0_Request / "dev-ops" / "log-cache" / "INFO").GET <@(user1)
val response = makeGetRequest(request)
Then("error should be " + UserHasMissingRoles + CanGetAllLevelLogsAtAllBanks)
response.code should equal(403)
response.body.extract[ErrorMessage].message should be(UserHasMissingRoles + CanGetAllLevelLogsAtAllBanks)
}
}

feature(s"test $ApiEndpoint1 version $VersionOfApi - Authorized access without pagination") {
scenario("We get log cache without pagination parameters", ApiEndpoint1, VersionOfApi) {
Given("We have a user with proper entitlement")

Check failure on line 50 in obp-api/src/test/scala/code/api/v5_1_0/LogCacheEndpointTest.scala

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "We have a user with proper entitlement" 10 times.

See more on https://sonarcloud.io/project/issues?id=OpenBankProject_OBP-API&issues=AZq_Mo2hSchGjxeKzdXD&open=AZq_Mo2hSchGjxeKzdXD&pullRequest=2634
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetAllLevelLogsAtAllBanks.toString)

When("We make a request to get log cache")
val request = (v5_1_0_Request / "dev-ops" / "log-cache" / "INFO").GET <@(user1)
val response = makeGetRequest(request)

Then("We should get a successful response")

Check failure on line 57 in obp-api/src/test/scala/code/api/v5_1_0/LogCacheEndpointTest.scala

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "We should get a successful response" 6 times.

See more on https://sonarcloud.io/project/issues?id=OpenBankProject_OBP-API&issues=AZq_Mo2hSchGjxeKzdXB&open=AZq_Mo2hSchGjxeKzdXB&pullRequest=2634
response.code should equal(200)
val json = response.body.extract[JObject]

And("The response should contain log entries")
(json \ "entries") should not be JNothing
}
}

feature(s"test $ApiEndpoint1 version $VersionOfApi - Authorized access with limit parameter") {
scenario("We get log cache with limit parameter only", ApiEndpoint1, VersionOfApi) {
Given("We have a user with proper entitlement")
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetAllLevelLogsAtAllBanks.toString)

When("We make a request with limit parameter")
val request = (v5_1_0_Request / "dev-ops" / "log-cache" / "INFO").GET <@(user1) <<? List(("limit", "5"))
val response = makeGetRequest(request)

Then("We should get a successful response")
response.code should equal(200)
val json = response.body.extract[JObject]

And("The response should contain limited log entries")
val entries = (json \ "entries").extract[JArray]
entries.values.asInstanceOf[List[_]].size should be <= 5
}
}

feature(s"test $ApiEndpoint1 version $VersionOfApi - Authorized access with offset parameter") {
scenario("We get log cache with offset parameter only", ApiEndpoint1, VersionOfApi) {
Given("We have a user with proper entitlement")
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetAllLevelLogsAtAllBanks.toString)

When("We make a request with offset parameter")
val request = (v5_1_0_Request / "dev-ops" / "log-cache" / "INFO").GET <@(user1) <<? List(("offset", "2"))
val response = makeGetRequest(request)

Then("We should get a successful response")
response.code should equal(200)
val json = response.body.extract[JObject]

And("The response should contain log entries starting from offset")
(json \ "entries") should not be JNothing
}
}

feature(s"test $ApiEndpoint1 version $VersionOfApi - Authorized access with both parameters") {
scenario("We get log cache with both limit and offset parameters", ApiEndpoint1, VersionOfApi) {
Given("We have a user with proper entitlement")
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetAllLevelLogsAtAllBanks.toString)

When("We make a request with both limit and offset parameters")
val request = (v5_1_0_Request / "dev-ops" / "log-cache" / "INFO").GET <@(user1) <<? List(("limit", "3"), ("offset", "1"))
val response = makeGetRequest(request)

Then("We should get a successful response")
response.code should equal(200)
val json = response.body.extract[JObject]

And("The response should contain paginated log entries")
val entries = (json \ "entries").extract[JArray]
entries.values.asInstanceOf[List[_]].size should be <= 3
}
}

feature(s"test $ApiEndpoint1 version $VersionOfApi - Edge cases") {
scenario("We get error with zero limit (invalid parameter)", ApiEndpoint1, VersionOfApi) {
Given("We have a user with proper entitlement")
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetAllLevelLogsAtAllBanks.toString)

When("We make a request with zero limit")
val request = (v5_1_0_Request / "dev-ops" / "log-cache" / "INFO").GET <@(user1) <<? List(("limit", "0"))
val response = makeGetRequest(request)

Then("We should get a bad request response")

Check failure on line 131 in obp-api/src/test/scala/code/api/v5_1_0/LogCacheEndpointTest.scala

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "We should get a bad request response" 6 times.

See more on https://sonarcloud.io/project/issues?id=OpenBankProject_OBP-API&issues=AZq_Mo2hSchGjxeKzdXA&open=AZq_Mo2hSchGjxeKzdXA&pullRequest=2634
response.code should equal(400)
val json = response.body.extract[JObject]

And("The response should contain the correct error message")
val message = (json \ "message").extract[String]
message should include("wrong value for obp_limit parameter")
}

scenario("We get log cache with large offset", ApiEndpoint1, VersionOfApi) {
Given("We have a user with proper entitlement")
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetAllLevelLogsAtAllBanks.toString)

When("We make a request with very large offset")
val request = (v5_1_0_Request / "dev-ops" / "log-cache" / "INFO").GET <@(user1) <<? List(("offset", "10000"))
val response = makeGetRequest(request)

Then("We should get a successful response")
response.code should equal(200)
val json = response.body.extract[JObject]

And("The response should not fail")
val entries = (json \ "entries").extract[JArray]
entries.values.asInstanceOf[List[_]].size should be >= 0
}

scenario("We get log cache with minimum valid limit", ApiEndpoint1, VersionOfApi) {
Given("We have a user with proper entitlement")
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetAllLevelLogsAtAllBanks.toString)

When("We make a request with minimum valid limit (1)")
val request = (v5_1_0_Request / "dev-ops" / "log-cache" / "INFO").GET <@(user1) <<? List(("limit", "1"))
val response = makeGetRequest(request)

Then("We should get a successful response")
response.code should equal(200)
val json = response.body.extract[JObject]

And("The response should contain at most 1 entry")
val entries = (json \ "entries").extract[JArray]
entries.values.asInstanceOf[List[_]].size should be <= 1
}
}

feature(s"test $ApiEndpoint1 version $VersionOfApi - Different log levels") {
scenario("We test different log levels with pagination", ApiEndpoint1, VersionOfApi) {
Given("We have a user with proper entitlement")
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetAllLevelLogsAtAllBanks.toString)

When("We make requests to different log levels with pagination")
val logLevels = List("DEBUG", "INFO", "WARN", "ERROR", "ALL")

logLevels.foreach { logLevel =>
val request = (v5_1_0_Request / "dev-ops" / "log-cache" / logLevel).GET <@(user1) <<? List(("limit", "2"), ("offset", "0"))
val response = makeGetRequest(request)

Then(s"We should get successful response for log level $logLevel")
response.code should equal(200)
val json = response.body.extract[JObject]

And("The response should have the correct structure")
(json \ "entries") should not be JNothing
}
}
}

feature(s"test $ApiEndpoint1 version $VersionOfApi - Invalid log level") {
scenario("We get error for invalid log level", ApiEndpoint1, VersionOfApi) {
Given("We have a user with proper entitlement")
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetAllLevelLogsAtAllBanks.toString)

When("We make a request with invalid log level")
val request = (v5_1_0_Request / "dev-ops" / "log-cache" / "INVALID_LEVEL").GET <@(user1)
val response = makeGetRequest(request)

Then("We should get a bad request response")
response.code should equal(400)
}
}

feature(s"test $ApiEndpoint1 version $VersionOfApi - Invalid parameters") {
scenario("We test invalid pagination parameters", ApiEndpoint1, VersionOfApi) {
Given("We have a user with proper entitlement")
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetAllLevelLogsAtAllBanks.toString)

When("We test with non-numeric limit parameter")
val requestInvalidLimit = (v5_1_0_Request / "dev-ops" / "log-cache" / "INFO").GET <@(user1) <<? List(("limit", "abc"))
val responseInvalidLimit = makeGetRequest(requestInvalidLimit)

Then("We should get a bad request response")
responseInvalidLimit.code should equal(400)

When("We test with non-numeric offset parameter")
val requestInvalidOffset = (v5_1_0_Request / "dev-ops" / "log-cache" / "INFO").GET <@(user1) <<? List(("offset", "xyz"))
val responseInvalidOffset = makeGetRequest(requestInvalidOffset)

Then("We should get a bad request response")
responseInvalidOffset.code should equal(400)

When("We test with negative limit parameter")
val requestNegativeLimit = (v5_1_0_Request / "dev-ops" / "log-cache" / "INFO").GET <@(user1) <<? List(("limit", "-1"))
val responseNegativeLimit = makeGetRequest(requestNegativeLimit)

Then("We should get a bad request response")
responseNegativeLimit.code should equal(400)

When("We test with negative offset parameter")
val requestNegativeOffset = (v5_1_0_Request / "dev-ops" / "log-cache" / "INFO").GET <@(user1) <<? List(("offset", "-1"))
val responseNegativeOffset = makeGetRequest(requestNegativeOffset)

Then("We should get a bad request response")
responseNegativeOffset.code should equal(400)
}
}
}
Loading