From 46aec84789afda52f579bd0cc4f10430e486aea6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Remseth?= Date: Wed, 27 Nov 2019 08:31:24 +0100 Subject: [PATCH 01/22] whitespace --- .../src/main/kotlin/org/ostelco/sim/es2plus/Es2PlusClient.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sim-administration/es2plus4dropwizard/src/main/kotlin/org/ostelco/sim/es2plus/Es2PlusClient.kt b/sim-administration/es2plus4dropwizard/src/main/kotlin/org/ostelco/sim/es2plus/Es2PlusClient.kt index a979a1a38..97e068961 100644 --- a/sim-administration/es2plus4dropwizard/src/main/kotlin/org/ostelco/sim/es2plus/Es2PlusClient.kt +++ b/sim-administration/es2plus4dropwizard/src/main/kotlin/org/ostelco/sim/es2plus/Es2PlusClient.kt @@ -255,7 +255,10 @@ class ES2PlusClient( returnValueClass = Es2ConfirmOrderResponse::class.java) } - fun cancelOrder(iccid: String, finalProfileStatusIndicator: String, eid: String? = null, matchingId: String? = null): HeaderOnlyResponse { + fun cancelOrder(iccid: String, + finalProfileStatusIndicator: String, + eid: String? = null, + matchingId: String? = null): HeaderOnlyResponse { return postEs2ProtocolCmd("/gsma/rsp2/es2plus/cancelOrder", es2ProtocolPayload = Es2CancelOrder( header = ES2RequestHeader( From 668a385456299ccad48e79f24362eeb96e307945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Remseth?= Date: Wed, 27 Nov 2019 08:33:35 +0100 Subject: [PATCH 02/22] A bit of documentation --- .../src/main/kotlin/org/ostelco/sim/es2plus/Es2PlusClient.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sim-administration/es2plus4dropwizard/src/main/kotlin/org/ostelco/sim/es2plus/Es2PlusClient.kt b/sim-administration/es2plus4dropwizard/src/main/kotlin/org/ostelco/sim/es2plus/Es2PlusClient.kt index 97e068961..532871fc8 100644 --- a/sim-administration/es2plus4dropwizard/src/main/kotlin/org/ostelco/sim/es2plus/Es2PlusClient.kt +++ b/sim-administration/es2plus4dropwizard/src/main/kotlin/org/ostelco/sim/es2plus/Es2PlusClient.kt @@ -119,7 +119,9 @@ class ES2PlusClient( return response } - /* For test cases where content should be returned. */ + /** + * For test cases where content should be returned. + */ @Throws(ES2PlusClientException::class) private fun postEs2ProtocolCmd( path: String, From e6a13885ffe97f776e5f50bdb1650cb751bc0892 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Remseth?= Date: Wed, 27 Nov 2019 16:11:50 +0100 Subject: [PATCH 03/22] Non-compiling first stab at making a poller (cleanup necessary) --- .../simcards/admin/PollOutstandingProfiles.kt | 134 ++++++++++++++++++ .../simcards/admin/SimAdministrationModule.kt | 4 + 2 files changed, 138 insertions(+) create mode 100644 sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PollOutstandingProfiles.kt diff --git a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PollOutstandingProfiles.kt b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PollOutstandingProfiles.kt new file mode 100644 index 000000000..91a397bb9 --- /dev/null +++ b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PollOutstandingProfiles.kt @@ -0,0 +1,134 @@ +package org.ostelco.simcards.admin + +import arrow.core.Either +import com.google.common.collect.ImmutableMultimap +import io.dropwizard.servlets.tasks.Task +import kotlinx.coroutines.newFixedThreadPoolContext +import org.ostelco.prime.getLogger +import org.ostelco.prime.jsonmapper.asJson +import org.ostelco.prime.model.SimProfile +import org.ostelco.prime.simmanager.SimManagerError +import org.ostelco.sim.es2plus.ES2PlusClient +import org.ostelco.sim.es2plus.ProfileStatus +import org.ostelco.simcards.inventory.SimInventoryDAO +import org.ostelco.simcards.inventory.SmDpPlusState +import java.io.PrintWriter + + +/** + * A dropwizard "task" that is intended to be invoked as an administrative step + * by an external agent that is part of the serving system, not a customer of it. + * + * The task implements a task that for all of the sim profiles in storage + * will search for profiles that has been allocated to end user equipment + * but has not yet been downloaded or installed by it, as seen by ES2+ + * callbacks. + * + * The functionality is duplicating some of the functions implemented by + * the ES2+ callback mechanism. The reason why we need this task in addition + * is that we have found the ES2+ callback to be unreliable. This task + * is therefore intended to serve as a fallback for that when working correctly + * preferable mechanism. + */ + +class PollOutstandingProfiles( + val simInventoryDAO: SimInventoryDAO, + val client: ES2PlusClient, + val profileVendors: List) : Task("poll_outstanding_profiles") { + + + private val logger by getLogger() + + + @Throws(Exception::class) + override fun execute(parameters: ImmutableMultimap, output: PrintWriter) { + // TODO: Rewrite to deliver a report of what happened, also if things went well. + pollAllocatedButNotDownloadedProfiles(output) + .mapLeft { simManagerError -> + logger.error(simManagerError.description) + output.println(asJson(simManagerError)) + } + } + + + private fun pollAllocatedButNotDownloadedProfiles(output: PrintWriter): Either = + simInventoryDAO.getAllocatedButNotDownloadedProfiles().flatMap { profilesToPoll -> + pollForSmdpStatus(output, profilesToPoll) + }.right() + + private fun pollForSmdpStatus(output: PrintWriter, profilesToPoll: List) { + + val result = StringBuffer() + + @Synchronized + fun reportln(s: String) { + output.println(s) + } + + fun sendReport() { + output.print(result.toString()) + } + + fun pollProfile(p: ProfileStatus) { + try { + + // Get state and iccid non null strings + val stateString = p.state ?: "" + val iccid = p.iccid ?: "" + + // Then discard empty states. + if (stateString == "") { + reportln("Empty state detected for iccid ${p.iccid}") + return + } + + val state = SmDpPlusState.valueOf(stateString.toUpperCase()) + + if (state != SmDpPlusState.RELEASED) { + val update = simInventoryDAO.setSmDpPlusStateUsingIccid(iccid, state) + if (update.isLeft()) { // TODO: This is is not idiomatic. Please help me make it idomatic before merging to develop. + update.mapLeft { + reportln("Could not update iccid=$iccid still set to ${state.name}. Sim manager error = $it") + } + } else { + // This probably represents an error situation in the SM-DP+, and _should_ perhaps + // be reported as an error by the sim manager. Please think about this and + // either change it to logger.error yourself, or ask me in review comments to do it for you. + logger.info("Updated state for iccid=$iccid still set to ${state.name}") + reportln("Updated state for iccid=$iccid still set to ${state.name}") + } + } else { + reportln("Just state for iccid=$iccid still set to ${state.name}") + } + + } catch (e: Exception) { + reportln("Couldn't fetch status for iccid ${p.iccid}", e) + } + } + + fun pollProfiles(statusList: List) { + // TODO: Rewrite this so that it runs up to 100 queries in parallell + // ES2+ is slow, so som paralellism is useful. Shouldn't be too much, since we + // might otherwise run out of file descriptors. + for (p in statusList) { + pollProfile(p) + } + } + + // First get the profile statuses + val statuses = client.profileStatus(profilesToPoll.map { it.iccId }) + val statusList = statuses.profileStatusList + + // Handle the error situation that we got a null response by reporting it both to + // the logger and the caller. + if (statusList == null) { + logger.error("Could not get statuses from SMDP+ for ") + reportln("Got null status list for iccid list $iccids") + return // TODO: Log som error situation first, also write to the response thingy + } + + // Happy day scenario. Update statuses, report results. + pollProfiles(statusList) + } +} + diff --git a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/SimAdministrationModule.kt b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/SimAdministrationModule.kt index f249697ba..d34cd4588 100644 --- a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/SimAdministrationModule.kt +++ b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/SimAdministrationModule.kt @@ -114,6 +114,10 @@ class SimAdministrationModule : PrimeModule { hssAdapterProxy = hssAdapters, profileVendors = config.profileVendors)) + env.admin().addTask(PollOutstandingProfilesTask( + simInventoryDAO = this.DAO, + httpClient = httpClient, + profileVendors = config.profileVendors)) env.healthChecks().register("smdp", SmdpPlusHealthceck(getDAO(), httpClient, config.profileVendors)) From 40e82843a3572f033779cf5915b2ede40b8f6f33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Remseth?= Date: Thu, 28 Nov 2019 12:48:32 +0100 Subject: [PATCH 04/22] Now compiles without error, about to start adding tests --- .../ocs/consumption/grpc/OcsGrpcServerTest.kt | 1 + .../simcards/admin/SimAdministrationTest.kt | 9 +- .../admin/PeriodicProvisioningTask.kt | 29 +------ ...iles.kt => PollOutstandingProfilesTask.kt} | 83 ++++++++++--------- .../simcards/admin/SimAdministrationModule.kt | 13 ++- .../simcards/inventory/SimInventoryDB.kt | 38 +++++---- .../inventory/SimInventoryDBWrapper.kt | 9 +- .../inventory/SimInventoryDBWrapperImpl.kt | 5 ++ .../ProfileVendorAdapterDatum.kt | 38 ++++++++- 9 files changed, 134 insertions(+), 91 deletions(-) rename sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/{PollOutstandingProfiles.kt => PollOutstandingProfilesTask.kt} (60%) diff --git a/ocs-ktc/src/test/kotlin/org/ostelco/prime/ocs/consumption/grpc/OcsGrpcServerTest.kt b/ocs-ktc/src/test/kotlin/org/ostelco/prime/ocs/consumption/grpc/OcsGrpcServerTest.kt index 208ecc1bd..c04869ce4 100644 --- a/ocs-ktc/src/test/kotlin/org/ostelco/prime/ocs/consumption/grpc/OcsGrpcServerTest.kt +++ b/ocs-ktc/src/test/kotlin/org/ostelco/prime/ocs/consumption/grpc/OcsGrpcServerTest.kt @@ -27,6 +27,7 @@ class OcsGrpcServerTest { fun `load test OCS using gRPC`() = runBlocking { // Add delay to DB call and skip analytics and low balance notification + @ExperimentalUnsignedTypes OnlineCharging.loadUnitTest = true server = OcsGrpcServer(8082, OcsGrpcService(OnlineCharging)) diff --git a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt index 34f775757..da7039f26 100644 --- a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt +++ b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt @@ -31,6 +31,7 @@ import org.ostelco.simcards.inventory.HssState import org.ostelco.simcards.inventory.ProvisionState import org.ostelco.simcards.inventory.SimEntry import org.ostelco.simcards.inventory.SimProfileKeyStatistics +import org.ostelco.simcards.profilevendors.ProfileVendorAdapterFactory import org.ostelco.simcards.smdpplus.SmDpPlusApplication import org.testcontainers.containers.BindMode import org.testcontainers.containers.FixedHostPortGenericContainer @@ -340,12 +341,14 @@ class SimAdministrationTest { noOfReleasedEntries = 0L, noOfUnallocatedEntries = 0L, noOfReservedEntries = 0L) + val pvaf = ProfileVendorAdapterFactory( + simInventoryDAO = simDao, + httpClient = httpClient, + profileVendors = ConfigRegistry.config.profileVendors) val task = PreallocateProfilesTask( - profileVendors = profileVendors, simInventoryDAO = simDao, - maxNoOfProfileToAllocate = maxNoOfProfilesToAllocate, hssAdapterProxy = hssAdapterCache, - httpClient = httpClient) + pvaf = pvaf) task.preAllocateSimProfiles() val postAllocationStats = diff --git a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PeriodicProvisioningTask.kt b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PeriodicProvisioningTask.kt index 518b60668..84c037ba4 100644 --- a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PeriodicProvisioningTask.kt +++ b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PeriodicProvisioningTask.kt @@ -6,7 +6,6 @@ import arrow.core.left import arrow.core.right import com.google.common.collect.ImmutableMultimap import io.dropwizard.servlets.tasks.Task -import org.apache.http.impl.client.CloseableHttpClient import org.ostelco.prime.getLogger import org.ostelco.prime.jsonmapper.asJson import org.ostelco.prime.simmanager.AdapterError @@ -20,7 +19,7 @@ import org.ostelco.simcards.inventory.ProvisionState import org.ostelco.simcards.inventory.SimEntry import org.ostelco.simcards.inventory.SimInventoryDAO import org.ostelco.simcards.inventory.SimProfileKeyStatistics -import org.ostelco.simcards.profilevendors.ProfileVendorAdapter +import org.ostelco.simcards.profilevendors.ProfileVendorAdapterFactory import java.io.PrintWriter import kotlin.math.min @@ -41,15 +40,12 @@ import kotlin.math.min class PreallocateProfilesTask( private val lowWaterMark: Int = 10, val maxNoOfProfileToAllocate: Int = 30, + val pvaf : ProfileVendorAdapterFactory, val simInventoryDAO: SimInventoryDAO, - val httpClient: CloseableHttpClient, - val hssAdapterProxy: SimManagerToHssDispatcherAdapter, - val profileVendors: List) : Task("preallocate_sim_profiles") { - + val hssAdapterProxy: SimManagerToHssDispatcherAdapter) : Task("preallocate_sim_profiles") { private val logger by getLogger() - @Throws(Exception::class) override fun execute(parameters: ImmutableMultimap, output: PrintWriter) { // TODO: Rewrite to deliver a report of what happened, also if things went well. @@ -60,30 +56,13 @@ class PreallocateProfilesTask( } } - private fun getConfigForVendorNamed(name: String) = - profileVendors.firstOrNull { - it.name == name - } - - private fun getProfileVendorAdapterForProfileVendorId(profileVendorId: Long): Either = - simInventoryDAO.getProfileVendorAdapterDatumById(profileVendorId) - .flatMap { datum -> - val profileVendorConfig = getConfigForVendorNamed(datum.name) - if (profileVendorConfig == null) { - AdapterError("profileVendorCondig null for profile vendor $profileVendorId, that's very bad.").left() - } else { - ProfileVendorAdapter(datum, profileVendorConfig, httpClient, simInventoryDAO).right() - } - } - - // TODO: This method must be refactored. It is still _way_ too complex. private fun preProvisionSimProfile(hssEntry: HssEntry, simEntry: SimEntry): Either = if (simEntry.id == null) { // TODO: This idiom is _bad_, find something better! AdapterError("simEntry.id == null for simEntry = '$simEntry'.").left() } else - getProfileVendorAdapterForProfileVendorId(simEntry.profileVendorId) + pvaf.getAdapterByVendorId(simEntry.profileVendorId) .flatMap { profileVendorAdapter -> when { simEntry.hssState == HssState.NOT_ACTIVATED -> { diff --git a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PollOutstandingProfiles.kt b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PollOutstandingProfilesTask.kt similarity index 60% rename from sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PollOutstandingProfiles.kt rename to sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PollOutstandingProfilesTask.kt index 91a397bb9..006b8dc81 100644 --- a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PollOutstandingProfiles.kt +++ b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PollOutstandingProfilesTask.kt @@ -3,18 +3,16 @@ package org.ostelco.simcards.admin import arrow.core.Either import com.google.common.collect.ImmutableMultimap import io.dropwizard.servlets.tasks.Task -import kotlinx.coroutines.newFixedThreadPoolContext import org.ostelco.prime.getLogger import org.ostelco.prime.jsonmapper.asJson -import org.ostelco.prime.model.SimProfile import org.ostelco.prime.simmanager.SimManagerError -import org.ostelco.sim.es2plus.ES2PlusClient import org.ostelco.sim.es2plus.ProfileStatus +import org.ostelco.simcards.inventory.SimEntry import org.ostelco.simcards.inventory.SimInventoryDAO import org.ostelco.simcards.inventory.SmDpPlusState +import org.ostelco.simcards.profilevendors.ProfileVendorAdapterFactory import java.io.PrintWriter - /** * A dropwizard "task" that is intended to be invoked as an administrative step * by an external agent that is part of the serving system, not a customer of it. @@ -31,15 +29,12 @@ import java.io.PrintWriter * preferable mechanism. */ -class PollOutstandingProfiles( +class PollOutstandingProfilesTask( val simInventoryDAO: SimInventoryDAO, - val client: ES2PlusClient, - val profileVendors: List) : Task("poll_outstanding_profiles") { - + val pvaf: ProfileVendorAdapterFactory) : Task("poll_outstanding_profiles") { private val logger by getLogger() - @Throws(Exception::class) override fun execute(parameters: ImmutableMultimap, output: PrintWriter) { // TODO: Rewrite to deliver a report of what happened, also if things went well. @@ -50,26 +45,29 @@ class PollOutstandingProfiles( } } + private fun pollAllocatedButNotDownloadedProfiles(output: PrintWriter): Either { + return simInventoryDAO.findAllocatedButNotDownloadedProfiles().map { profilesToPoll -> + pollForSmdpStatus(output, profilesToPoll) + } + } - private fun pollAllocatedButNotDownloadedProfiles(output: PrintWriter): Either = - simInventoryDAO.getAllocatedButNotDownloadedProfiles().flatMap { profilesToPoll -> - pollForSmdpStatus(output, profilesToPoll) - }.right() - - private fun pollForSmdpStatus(output: PrintWriter, profilesToPoll: List) { + private fun pollForSmdpStatus(output: PrintWriter, profilesToPoll: List) { val result = StringBuffer() @Synchronized - fun reportln(s: String) { + fun reportln(s: String, e: Exception? = null) { output.println(s) + if (e != null) { + output.println(" Caused exception: $e") + } } fun sendReport() { output.print(result.toString()) } - fun pollProfile(p: ProfileStatus) { + fun updateProfileInDb(p: ProfileStatus) { try { // Get state and iccid non null strings @@ -94,41 +92,44 @@ class PollOutstandingProfiles( // This probably represents an error situation in the SM-DP+, and _should_ perhaps // be reported as an error by the sim manager. Please think about this and // either change it to logger.error yourself, or ask me in review comments to do it for you. - logger.info("Updated state for iccid=$iccid still set to ${state.name}") - reportln("Updated state for iccid=$iccid still set to ${state.name}") + val report = "Updated state for iccid=$iccid still set to ${state.name}" + logger.info(report) + reportln(report) } } else { - reportln("Just state for iccid=$iccid still set to ${state.name}") + reportln("State for iccid=$iccid still set to ${state.name}") } - } catch (e: Exception) { - reportln("Couldn't fetch status for iccid ${p.iccid}", e) + reportln("Couldn't update status for iccid ${p.iccid}", e) } } - fun pollProfiles(statusList: List) { - // TODO: Rewrite this so that it runs up to 100 queries in parallell - // ES2+ is slow, so som paralellism is useful. Shouldn't be too much, since we - // might otherwise run out of file descriptors. - for (p in statusList) { - pollProfile(p) + fun asVendorIdToIccdList(profiles: List): Map> { + val iccidsByProfileVendorIdMap : MutableMap> + = mutableMapOf>() + profiles.forEach { + simEntry: SimEntry -> + val vendorId = simEntry.profileVendorId + if (!iccidsByProfileVendorIdMap.containsKey(vendorId)) { + iccidsByProfileVendorIdMap[vendorId] = mutableListOf() + } + iccidsByProfileVendorIdMap[vendorId]?.add(simEntry.iccid) } + return iccidsByProfileVendorIdMap } - // First get the profile statuses - val statuses = client.profileStatus(profilesToPoll.map { it.iccId }) - val statusList = statuses.profileStatusList - - // Handle the error situation that we got a null response by reporting it both to - // the logger and the caller. - if (statusList == null) { - logger.error("Could not get statuses from SMDP+ for ") - reportln("Got null status list for iccid list $iccids") - return // TODO: Log som error situation first, also write to the response thingy + // Then poll them individually via the profile vendor adapter associated with the + // profile (there is not much to be gained here by doing it in paralellel, but perhaps + // one day it will be). + + for ((vendorId, iccidList) in asVendorIdToIccdList(profilesToPoll)) { + pvaf.getAdapterByVendorId(vendorId).mapRight { profileVendorAdapter -> + val statuses = profileVendorAdapter.getProfileStatusList(iccidList) + statuses.mapRight{ + it.forEach { + updateProfileInDb(it)}} + } // TODO: Am I missing the error situation here? } - - // Happy day scenario. Update statuses, report results. - pollProfiles(statusList) } } diff --git a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/SimAdministrationModule.kt b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/SimAdministrationModule.kt index d34cd4588..c10998792 100644 --- a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/SimAdministrationModule.kt +++ b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/SimAdministrationModule.kt @@ -30,6 +30,7 @@ import org.ostelco.simcards.inventory.SimInventoryDAO import org.ostelco.simcards.inventory.SimInventoryDB import org.ostelco.simcards.inventory.SimInventoryDBWrapperImpl import org.ostelco.simcards.inventory.SimInventoryResource +import org.ostelco.simcards.profilevendors.ProfileVendorAdapterFactory /** * The SIM manager @@ -108,16 +109,20 @@ class SimAdministrationModule : PrimeModule { simInventoryDAO = this.DAO ) - env.admin().addTask(PreallocateProfilesTask( + val pvaf = ProfileVendorAdapterFactory( simInventoryDAO = this.DAO, httpClient = httpClient, + profileVendors = config.profileVendors + ) + + env.admin().addTask(PreallocateProfilesTask( + simInventoryDAO = this.DAO, hssAdapterProxy = hssAdapters, - profileVendors = config.profileVendors)) + pvaf = pvaf)) env.admin().addTask(PollOutstandingProfilesTask( simInventoryDAO = this.DAO, - httpClient = httpClient, - profileVendors = config.profileVendors)) + pvaf = pvaf)) env.healthChecks().register("smdp", SmdpPlusHealthceck(getDAO(), httpClient, config.profileVendors)) diff --git a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/inventory/SimInventoryDB.kt b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/inventory/SimInventoryDB.kt index d715620a2..c20335900 100644 --- a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/inventory/SimInventoryDB.kt +++ b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/inventory/SimInventoryDB.kt @@ -234,7 +234,6 @@ interface SimInventoryDB { fun getProfileNamesForHss(hssId: Long): List - // WHERE hlrId = 2 AND profile = 'OYA_M1_BF76' AND // smdpPlusState <> 'DOWNLOADED' AND smdpPlusState <> 'INSTALLED' AND smdpPlusState <> 'ENABLED' AND provisionState = 'AVAILABLE' AND matchingid is null @@ -283,7 +282,6 @@ interface SimInventoryDB { provisionedAvailableState: String = ProvisionState.AVAILABLE.name): List - @SqlQuery(""" SELECT DISTINCT profile AS simprofilename, hlrid AS hssid, hlr_adapters.name AS hssname FROM sim_entries, hlr_adapters WHERE hlrid=hlr_adapters.id """) @@ -291,6 +289,22 @@ interface SimInventoryDB { fun getHssProfileNamePairs(): List + /** + * Row mapper for the getHssProfileNamePairs method. + */ + class HssProfileNameMapper : RowMapper { + override fun map(row: ResultSet, ctx: StatementContext): HssProfileIdName? { + if (row.isAfterLast) { + return null + } + + val hssId = row.getLong("hssid") + val hssName = row.getString("hssname") + val simProfileName = row.getString("simprofilename") + return HssProfileIdName(hssId = hssId, hssName = hssName, simProfileName = simProfileName) + } + } + /** * Golden numbers are numbers ending in either "0000" or "9999", and they have to be * treated specially. @@ -299,18 +313,14 @@ interface SimInventoryDB { WHERE batch = :batchId AND msisdn ~ '[0-9]*(0000|9999)$' """) fun reserveGoldenNumbersForBatch(batchId: Long, provisionReservedState: ProvisionState = ProvisionState.RESERVED): Int -} - -class HssProfileNameMapper : RowMapper { - override fun map(row: ResultSet, ctx: StatementContext): HssProfileIdName? { - if (row.isAfterLast) { - return null - } - val hssId = row.getLong("hssid") - val hssName = row.getString("hssname") - val simProfileName = row.getString("simprofilename") - return HssProfileIdName(hssId = hssId, hssName = hssName, simProfileName = simProfileName) - } + /** + * Return a list of all sim profiles that are both priovisioned for a specific + * end user equipment, and still in SMDP sttr 'RELEASED' meaning that it hasn't + * been downloaded, installed or deleted yet. + */ + @SqlQuery("""SELECT * FROM sim_entries + WHERE provisionstate = 'PROVISIONED' and smdpplusstate = 'RELEASED'""") + fun findAllocatedButNotDownloadedProfiles(): List } diff --git a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/inventory/SimInventoryDBWrapper.kt b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/inventory/SimInventoryDBWrapper.kt index 71369799b..ee839a44c 100644 --- a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/inventory/SimInventoryDBWrapper.kt +++ b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/inventory/SimInventoryDBWrapper.kt @@ -152,10 +152,15 @@ interface SimInventoryDBWrapper { fun reserveGoldenNumbersForBatch(batchId: Long): Either /** - * Return a list of sim Profile names associated with HSSes. Return both the - * HSSId (database internal ID), and the public name of the HSS. + * Return a list of (HSS ID, hss Name, sim profile name) tuples. */ fun getHssProfileNamePairs(): Either> + + /** + * Return a list of sim entries representing profiles that has been allocated + * to end user equipment, but has not + */ + fun findAllocatedButNotDownloadedProfiles(): Either> } /** diff --git a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/inventory/SimInventoryDBWrapperImpl.kt b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/inventory/SimInventoryDBWrapperImpl.kt index d7f368c3d..dde2cf44e 100644 --- a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/inventory/SimInventoryDBWrapperImpl.kt +++ b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/inventory/SimInventoryDBWrapperImpl.kt @@ -60,6 +60,11 @@ class SimInventoryDBWrapperImpl(private val db: SimInventoryDB) : SimInventoryDB db.findNextReadyToUseSimProfileForHlr(hssId, profile) } + override fun findAllocatedButNotDownloadedProfiles(): Either> = + either(NotFoundError("Failure while getting allocated but nt downloaded profiles")) { + db.findAllocatedButNotDownloadedProfiles() + } + @Transaction override fun setEidOfSimProfileByIccid(iccid: String, eid: String): Either = either(NotFoundError("Found no SIM profile with ICCID $iccid update of EID failed")) { diff --git a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/profilevendors/ProfileVendorAdapterDatum.kt b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/profilevendors/ProfileVendorAdapterDatum.kt index 8fdc404fe..89030fe9a 100644 --- a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/profilevendors/ProfileVendorAdapterDatum.kt +++ b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/profilevendors/ProfileVendorAdapterDatum.kt @@ -25,6 +25,29 @@ import org.ostelco.simcards.inventory.SmDpPlusState import java.net.URL +class ProfileVendorAdapterFactory( + val simInventoryDAO: SimInventoryDAO, + val httpClient: CloseableHttpClient, + val profileVendors: List) { + + // Give this a shorter name + fun getAdapterByVendorId(profileVendorId: Long): Either = + simInventoryDAO.getProfileVendorAdapterDatumById(profileVendorId) + .flatMap { datum -> + val profileVendorConfig = getConfigForVendorNamed(datum.name) + if (profileVendorConfig == null) { + AdapterError("profileVendorCondig null for profile vendor $profileVendorId, that's very bad.").left() + } else { + ProfileVendorAdapter(datum, profileVendorConfig, httpClient, simInventoryDAO).right() + } + } + + private fun getConfigForVendorNamed(name: String) = + profileVendors.firstOrNull { + it.name == name + } +} + // TODO: Why on earth is the json property set to "metricName"? It makes no sense. // Fix it, but understand what it means. data class ProfileVendorAdapterDatum( @@ -226,9 +249,8 @@ data class ProfileVendorAdapter( /** * Downloads the SM-DP+ 'profile status' information for a list of ICCIDs * from a SM-DP+ service. - * @param httpClient HTTP client - * @param config SIM vendor specific configuration * @param iccidList list with ICCID + * @param expectSuccess True iff no null values are to be expected. * @return A list with SM-DP+ 'profile status' information */ private fun getProfileStatus( @@ -244,6 +266,16 @@ data class ProfileVendorAdapter( } + + /** + * Return a list of profile statuses for a parameter list of profiles. Signal an error + * if an attempt is made at getting the status of a profile for which there is no known + * record + */ + fun getProfileStatusList(iccidList: List): Either> { + return getProfileStatus(iccidList = iccidList, expectSuccess = true) + } + /// /// Activating a sim card. /// @@ -278,4 +310,6 @@ data class ProfileVendorAdapter( */ fun ping(): Either> = getProfileStatus(iccidList = listContainingOnlyInvalidIccid, expectSuccess = false) + + } \ No newline at end of file From 8ae86c673a9fff59be103a4fa3903dd9254d734e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Remseth?= Date: Thu, 28 Nov 2019 15:18:23 +0100 Subject: [PATCH 05/22] Fix bug: Had removed parameter, that led to a failing test! --- .../kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt index da7039f26..5b0b83b39 100644 --- a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt +++ b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt @@ -346,6 +346,7 @@ class SimAdministrationTest { httpClient = httpClient, profileVendors = ConfigRegistry.config.profileVendors) val task = PreallocateProfilesTask( + maxNoOfProfileToAllocate = maxNoOfProfilesToAllocate, simInventoryDAO = simDao, hssAdapterProxy = hssAdapterCache, pvaf = pvaf) From e62d8e27e79afe87dcb835a347c9e2f11d68d208 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Remseth?= Date: Thu, 28 Nov 2019 16:05:27 +0100 Subject: [PATCH 06/22] Minimal test (don't throw runtime exceptions while running) --- .../simcards/admin/SimAdministrationTest.kt | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt index 5b0b83b39..7ecc9f984 100644 --- a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt +++ b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt @@ -2,6 +2,7 @@ package org.ostelco.simcards.admin import arrow.core.Either import com.codahale.metrics.health.HealthCheck +import com.google.common.collect.ImmutableMultimap import com.google.gson.JsonElement import com.google.gson.JsonObject import com.google.gson.JsonParser @@ -38,6 +39,8 @@ import org.testcontainers.containers.FixedHostPortGenericContainer import org.testcontainers.containers.PostgreSQLContainer import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy import java.io.FileInputStream +import java.io.PrintWriter +import java.io.StringWriter import java.time.Duration import java.time.temporal.ChronoUnit import javax.ws.rs.client.Client @@ -304,6 +307,36 @@ class SimAdministrationTest { } } + + @Test + fun testPollingOfOutstandingProfilesTask() { + loadSimData() + val simDao = SIM_MANAGER_RULE.getApplication() + .getDAO() + + val profileVendors = SIM_MANAGER_RULE.configuration.profileVendors + val hssConfigs = SIM_MANAGER_RULE.configuration.hssVendors + val httpClient = HttpClientBuilder(SIM_MANAGER_RULE.environment) + .build("poll_outstanding_profiles") + + val pvaf = ProfileVendorAdapterFactory( + simInventoryDAO = simDao, + httpClient = httpClient, + profileVendors = ConfigRegistry.config.profileVendors) + + val task = PollOutstandingProfilesTask(simInventoryDAO = simDao, pvaf = pvaf) + + + val out = StringWriter(); + val writer = PrintWriter(out); + val parameters = ImmutableMultimap.builder().build() + task.execute(parameters, writer) + + // TODO: If we can get to this point without failure, we will then add + // some assertions that should be true after the task execution. + + } + @Test fun testPeriodicProvisioningTask() { loadSimData() From 2ab97aefe7e8eebd8003ec489b50581319c19f42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Remseth?= Date: Thu, 28 Nov 2019 16:23:26 +0100 Subject: [PATCH 07/22] extend pollng test to prepare profiles for allocation, not yet allocating anything though. --- .../simcards/admin/SimAdministrationTest.kt | 49 +++++++++++++++---- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt index 7ecc9f984..0b7cfa7c0 100644 --- a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt +++ b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt @@ -55,7 +55,6 @@ class SimAdministrationTest { private val phoneType = "rababara" private val expectedProfile = "IPHONE_PROFILE_2" - companion object { private lateinit var jdbi: Jdbi private lateinit var client: Client @@ -172,7 +171,6 @@ class SimAdministrationTest { dao.clearTables() } - private fun presetTables() { val dao = SIM_MANAGER_RULE.getApplication().getDAO() @@ -190,9 +188,7 @@ class SimAdministrationTest { } val response = - target - .request() - .put(Entity.entity(entries, MediaType.TEXT_PLAIN)) + target.request().put(Entity.entity(entries, MediaType.TEXT_PLAIN)) assertThat(response.status).isEqualTo(expectedReturnCode) } @@ -317,24 +313,59 @@ class SimAdministrationTest { val profileVendors = SIM_MANAGER_RULE.configuration.profileVendors val hssConfigs = SIM_MANAGER_RULE.configuration.hssVendors val httpClient = HttpClientBuilder(SIM_MANAGER_RULE.environment) - .build("poll_outstanding_profiles") + .build("periodicProvisioningTaskClient") + val maxNoOfProfilesToAllocate = 10 + + val hlrs = simDao.getHssEntries() + assertThat(hlrs.isRight()).isTrue() + + var hssId: Long = 0 + hlrs.map { + hssId = it[0].id + } + val dispatcher = DirectHssDispatcher( + hssConfigs = hssConfigs, + httpClient = httpClient, + healthCheckRegistrar = object : HealthCheckRegistrar { + override fun registerHealthCheck(name: String, healthCheck: HealthCheck) { + SIM_MANAGER_RULE.environment.healthChecks().register(name, healthCheck) + } + }) + val hssAdapterCache = SimManagerToHssDispatcherAdapter( + dispatcher = dispatcher, + simInventoryDAO = simDao) + val preStats = SimProfileKeyStatistics( + noOfEntries = 0L, + noOfEntriesAvailableForImmediateUse = 0L, + noOfReleasedEntries = 0L, + noOfUnallocatedEntries = 0L, + noOfReservedEntries = 0L) val pvaf = ProfileVendorAdapterFactory( simInventoryDAO = simDao, httpClient = httpClient, profileVendors = ConfigRegistry.config.profileVendors) + val preallocationTask = PreallocateProfilesTask( + maxNoOfProfileToAllocate = maxNoOfProfilesToAllocate, + simInventoryDAO = simDao, + hssAdapterProxy = hssAdapterCache, + pvaf = pvaf) + - val task = PollOutstandingProfilesTask(simInventoryDAO = simDao, pvaf = pvaf) + preallocationTask.preAllocateSimProfiles() // TODO: Should be rewritten, only assume "execute" available. + val pollingTask = PollOutstandingProfilesTask(simInventoryDAO = simDao, pvaf = pvaf) val out = StringWriter(); val writer = PrintWriter(out); val parameters = ImmutableMultimap.builder().build() - task.execute(parameters, writer) + pollingTask.execute(parameters, writer) + + println("==> '${out.toString()}'") // TODO: If we can get to this point without failure, we will then add // some assertions that should be true after the task execution. - + } @Test From aea25b4ce1fb50787ac22d3b33929beb2eb1b2e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Remseth?= Date: Thu, 28 Nov 2019 19:42:41 +0100 Subject: [PATCH 08/22] Probably, mostly _complete_, but lala failing test. --- .../simcards/admin/SimAdministrationTest.kt | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt index 0b7cfa7c0..fd324c352 100644 --- a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt +++ b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt @@ -31,6 +31,7 @@ import org.ostelco.simcards.hss.SimManagerToHssDispatcherAdapter import org.ostelco.simcards.inventory.HssState import org.ostelco.simcards.inventory.ProvisionState import org.ostelco.simcards.inventory.SimEntry +import org.ostelco.simcards.inventory.SimInventoryApi import org.ostelco.simcards.inventory.SimProfileKeyStatistics import org.ostelco.simcards.profilevendors.ProfileVendorAdapterFactory import org.ostelco.simcards.smdpplus.SmDpPlusApplication @@ -351,11 +352,26 @@ class SimAdministrationTest { hssAdapterProxy = hssAdapterCache, pvaf = pvaf) - preallocationTask.preAllocateSimProfiles() // TODO: Should be rewritten, only assume "execute" available. val pollingTask = PollOutstandingProfilesTask(simInventoryDAO = simDao, pvaf = pvaf) + // Wrap in a method that executes the task and returns the result-string, use that + // for the preallocation task also. + + val hssName = ConfigRegistry?.config.profileVendors[0]?.name + assertNotNull(hssName) + + // Allocate the next available simcard + val config = SIM_MANAGER_RULE.getApplication() + val simApi: SimInventoryApi = SimInventoryApi(httpClient, SIM_MANAGER_RULE.configuration, simDao) + + val simEntry = simApi.allocateNextEsimProfile(hssName, "LOLTEL_IPHONE_1") + .fold({null}, {it}) + + assertNotNull(simEntry) + + // Then run the polling task and see what happens. val out = StringWriter(); val writer = PrintWriter(out); val parameters = ImmutableMultimap.builder().build() @@ -365,7 +381,6 @@ class SimAdministrationTest { // TODO: If we can get to this point without failure, we will then add // some assertions that should be true after the task execution. - } @Test From 985cd20e7386e657f9dabe6aa0b7efd6e1eb1d74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Remseth?= Date: Thu, 28 Nov 2019 20:34:04 +0100 Subject: [PATCH 09/22] Use the right profile name --- .../org/ostelco/simcards/admin/SimAdministrationTest.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt index fd324c352..05860f3b5 100644 --- a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt +++ b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt @@ -366,7 +366,9 @@ class SimAdministrationTest { val config = SIM_MANAGER_RULE.getApplication() val simApi: SimInventoryApi = SimInventoryApi(httpClient, SIM_MANAGER_RULE.configuration, simDao) - val simEntry = simApi.allocateNextEsimProfile(hssName, "LOLTEL_IPHONE_1") + // TODO: Rig this so that callback can be disabled, so that we can test + // the polling usecase. + val simEntry = simApi.allocateNextEsimProfile(hssName, "IPHONE_PROFILE_2") .fold({null}, {it}) assertNotNull(simEntry) From c2c93dab3b92db19edb1a33f6e5fe525772cab4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Remseth?= Date: Thu, 28 Nov 2019 21:35:15 +0100 Subject: [PATCH 10/22] testcase still passing, but it still doesn't actually test the case where the new functionality would apply, so it still needs to be amended a bit. --- .../org/ostelco/simcards/admin/SimAdministrationTest.kt | 4 ++-- .../ostelco/simcards/admin/PollOutstandingProfilesTask.kt | 3 +-- .../main/kotlin/org/ostelco/simcards/admin/SimManager.kt | 6 +++--- .../org/ostelco/simcards/inventory/SimInventoryApi.kt | 8 ++++---- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt index 05860f3b5..0211c0e4a 100644 --- a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt +++ b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt @@ -359,7 +359,7 @@ class SimAdministrationTest { // Wrap in a method that executes the task and returns the result-string, use that // for the preallocation task also. - val hssName = ConfigRegistry?.config.profileVendors[0]?.name + val hssName = ConfigRegistry?.config.hssVendors[0].name assertNotNull(hssName) // Allocate the next available simcard @@ -368,7 +368,7 @@ class SimAdministrationTest { // TODO: Rig this so that callback can be disabled, so that we can test // the polling usecase. - val simEntry = simApi.allocateNextEsimProfile(hssName, "IPHONE_PROFILE_2") + val simEntry = simApi.allocateNextEsimProfile(hssName = hssName, phoneType = "nokia") .fold({null}, {it}) assertNotNull(simEntry) diff --git a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PollOutstandingProfilesTask.kt b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PollOutstandingProfilesTask.kt index 006b8dc81..bd30ba65f 100644 --- a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PollOutstandingProfilesTask.kt +++ b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PollOutstandingProfilesTask.kt @@ -126,8 +126,7 @@ class PollOutstandingProfilesTask( pvaf.getAdapterByVendorId(vendorId).mapRight { profileVendorAdapter -> val statuses = profileVendorAdapter.getProfileStatusList(iccidList) statuses.mapRight{ - it.forEach { - updateProfileInDb(it)}} + it.forEach { updateProfileInDb(it) } } } // TODO: Am I missing the error situation here? } } diff --git a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/SimManager.kt b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/SimManager.kt index eb3a9aeb8..262337b09 100644 --- a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/SimManager.kt +++ b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/SimManager.kt @@ -15,10 +15,10 @@ object SimManagerSingleton : SimManager { private val logger by getLogger() - override fun allocateNextEsimProfile(hlr: String, phoneType: String?): Either = - simInventoryApi.allocateNextEsimProfile(hlrName = hlr, phoneType = "$hlr.${phoneType ?: "generic"}").bimap( + override fun allocateNextEsimProfile(hssName: String, phoneType: String?): Either = + simInventoryApi.allocateNextEsimProfile(hssName = hssName, phoneType = "$hssName.${phoneType ?: "generic"}").bimap( { - "Failed to allocate eSIM for HLR - $hlr for phoneType - $phoneType" + "Failed to allocate eSIM for HLR - $hssName for phoneType - $phoneType" }, { simEntry -> mapToModelSimEntry(simEntry) }) diff --git a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/inventory/SimInventoryApi.kt b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/inventory/SimInventoryApi.kt index 2b11816f7..429b6bb0d 100644 --- a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/inventory/SimInventoryApi.kt +++ b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/inventory/SimInventoryApi.kt @@ -65,14 +65,14 @@ class SimInventoryApi(private val httpClient: CloseableHttpClient, } } - fun allocateNextEsimProfile(hlrName: String, phoneType: String): Either = + fun allocateNextEsimProfile(hssName: String, phoneType: String): Either = IO { Either.monad().binding { - logger.info("Allocating new SIM for hlr ${hlrName} and phone-type ${phoneType}") + logger.info("Allocating new SIM for hlr ${hssName} and phone-type ${phoneType}") - val hlrAdapter = dao.getHssEntryByName(hlrName) + val hlrAdapter = dao.getHssEntryByName(hssName) .bind() - val profile = getProfileType(hlrName, phoneType) + val profile = getProfileType(hssName, phoneType) .bind() val simEntry = dao.findNextReadyToUseSimProfileForHss(hlrAdapter.id, profile) .bind() From 31f4e1ac4d0a7159c4fbef05db898cd0e6ae383a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Remseth?= Date: Thu, 28 Nov 2019 21:59:27 +0100 Subject: [PATCH 11/22] Add TODOs indicating what we should do in order to get a test that actually tests the functionality of the poller. --- .../simcards/admin/SimAdministrationTest.kt | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt index 0211c0e4a..2b619a0af 100644 --- a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt +++ b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt @@ -366,8 +366,8 @@ class SimAdministrationTest { val config = SIM_MANAGER_RULE.getApplication() val simApi: SimInventoryApi = SimInventoryApi(httpClient, SIM_MANAGER_RULE.configuration, simDao) - // TODO: Rig this so that callback can be disabled, so that we can test - // the polling usecase. + // TODO: Run the polling, and observe that nothing happens. + val simEntry = simApi.allocateNextEsimProfile(hssName = hssName, phoneType = "nokia") .fold({null}, {it}) @@ -379,10 +379,14 @@ class SimAdministrationTest { val parameters = ImmutableMultimap.builder().build() pollingTask.execute(parameters, writer) - println("==> '${out.toString()}'") + // First real verification. Check that nothing has changed so far, neither in the + // SMDP or in the database. + val outString = out.toString() + assertTrue(outString.contains("State for iccid=8901000000000000977 still set to RELEASED")) - // TODO: If we can get to this point without failure, we will then add - // some assertions that should be true after the task execution. + // TODO: Simulate that ICCID 8901000000000000977 is downloaded, but no callback is sent! + // TODO: Again check the value of the value by polling, and possibly updating the value. + // TODO: Again, check that the test ran, but nothing happened. } @Test From 581844ff5fa144a45d70846a4dd16304526f1cae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Remseth?= Date: Fri, 29 Nov 2019 07:48:54 +0100 Subject: [PATCH 12/22] Factoring out task invocation into a class. Step 1: extract cocmmonalities, in preparation of removing them where they are duplicated. --- .../simcards/admin/SimAdministrationTest.kt | 74 ++++++++++++++++++- 1 file changed, 71 insertions(+), 3 deletions(-) diff --git a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt index 2b619a0af..8063aa0cd 100644 --- a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt +++ b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt @@ -12,6 +12,7 @@ import io.dropwizard.jdbi3.JdbiFactory import io.dropwizard.testing.ConfigOverride import io.dropwizard.testing.ResourceHelpers import io.dropwizard.testing.junit.DropwizardAppRule +import org.apache.http.impl.client.CloseableHttpClient import org.assertj.core.api.Assertions.assertThat import org.glassfish.jersey.client.ClientProperties import org.jdbi.v3.core.Jdbi @@ -32,6 +33,7 @@ import org.ostelco.simcards.inventory.HssState import org.ostelco.simcards.inventory.ProvisionState import org.ostelco.simcards.inventory.SimEntry import org.ostelco.simcards.inventory.SimInventoryApi +import org.ostelco.simcards.inventory.SimInventoryDAO import org.ostelco.simcards.inventory.SimProfileKeyStatistics import org.ostelco.simcards.profilevendors.ProfileVendorAdapterFactory import org.ostelco.simcards.smdpplus.SmDpPlusApplication @@ -181,7 +183,7 @@ class SimAdministrationTest { } /* The SIM dataset is the same that is used by the SM-DP+ emulator. */ - private fun loadSimData(hssState: HssState? = null, queryParameterName: String = "initialHssState", expectedReturnCode: Int = 200) { + protected fun loadSimData(hssState: HssState? = null, queryParameterName: String = "initialHssState", expectedReturnCode: Int = 200) { val entries = FileInputStream(SM_DP_PLUS_RULE.configuration.simBatchData) var target = client.target("$simManagerEndpoint/$hssName/import-batch/profilevendor/$profileVendor") if (hssState != null) { @@ -304,6 +306,70 @@ class SimAdministrationTest { } } + class TaskInvocationFixture(val testClass: SimAdministrationTest) { + var simDao: SimInventoryDAO + var profileVendors: List + var hssConfigs: List + var httpClient : CloseableHttpClient + var maxNoOfProfilesToAllocate = 10 + var dispatcher: DirectHssDispatcher + var hssAdapterCache: SimManagerToHssDispatcherAdapter + var preStats: SimProfileKeyStatistics + var pvaf: ProfileVendorAdapterFactory + var preallocationTask: PreallocateProfilesTask + + val simApi: SimInventoryApi + + init { + testClass.loadSimData() + + simDao = SIM_MANAGER_RULE.getApplication() + .getDAO() + + profileVendors = SIM_MANAGER_RULE.configuration.profileVendors + hssConfigs = SIM_MANAGER_RULE.configuration.hssVendors + httpClient = HttpClientBuilder(SIM_MANAGER_RULE.environment) + .build("periodicProvisioningTaskClient") + maxNoOfProfilesToAllocate = 10 + + val hlrs = simDao.getHssEntries() + assertThat(hlrs.isRight()).isTrue() + + var hssId: Long = 0 + hlrs.map { + hssId = it[0].id + } + + dispatcher = DirectHssDispatcher( + hssConfigs = hssConfigs, + httpClient = httpClient, + healthCheckRegistrar = object : HealthCheckRegistrar { + override fun registerHealthCheck(name: String, healthCheck: HealthCheck) { + SIM_MANAGER_RULE.environment.healthChecks().register(name, healthCheck) + } + }) + hssAdapterCache = SimManagerToHssDispatcherAdapter( + dispatcher = dispatcher, + simInventoryDAO = simDao) + preStats = SimProfileKeyStatistics( + noOfEntries = 0L, + noOfEntriesAvailableForImmediateUse = 0L, + noOfReleasedEntries = 0L, + noOfUnallocatedEntries = 0L, + noOfReservedEntries = 0L) + pvaf = ProfileVendorAdapterFactory( + simInventoryDAO = simDao, + httpClient = httpClient, + profileVendors = ConfigRegistry.config.profileVendors) + preallocationTask = PreallocateProfilesTask( + maxNoOfProfileToAllocate = maxNoOfProfilesToAllocate, + simInventoryDAO = simDao, + hssAdapterProxy = hssAdapterCache, + pvaf = pvaf) + simApi = SimInventoryApi(httpClient, SIM_MANAGER_RULE.configuration, simDao) + } + } + @Test fun testPollingOfOutstandingProfilesTask() { @@ -363,13 +429,13 @@ class SimAdministrationTest { assertNotNull(hssName) // Allocate the next available simcard - val config = SIM_MANAGER_RULE.getApplication() + // val config = SIM_MANAGER_RULE.getApplication() val simApi: SimInventoryApi = SimInventoryApi(httpClient, SIM_MANAGER_RULE.configuration, simDao) // TODO: Run the polling, and observe that nothing happens. val simEntry = simApi.allocateNextEsimProfile(hssName = hssName, phoneType = "nokia") - .fold({null}, {it}) + .fold({ null }, { it }) assertNotNull(simEntry) @@ -387,8 +453,10 @@ class SimAdministrationTest { // TODO: Simulate that ICCID 8901000000000000977 is downloaded, but no callback is sent! // TODO: Again check the value of the value by polling, and possibly updating the value. // TODO: Again, check that the test ran, but nothing happened. + // TODO: Perhaps, make the verifications a bit less brittle than grepping in a string? } + @Test fun testPeriodicProvisioningTask() { loadSimData() From d1778ea7468211ff2686d4a319e97c3de8efcc2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Remseth?= Date: Fri, 29 Nov 2019 08:53:33 +0100 Subject: [PATCH 13/22] Refacgored testPollingOfOutstandingProfilesTask to use the TaskInvocationFixture for greater conciseness --- .../simcards/admin/SimAdministrationTest.kt | 101 ++++++------------ 1 file changed, 33 insertions(+), 68 deletions(-) diff --git a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt index 8063aa0cd..1ed9284a4 100644 --- a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt +++ b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt @@ -320,6 +320,8 @@ class SimAdministrationTest { val simApi: SimInventoryApi + val pollingTask : PollOutstandingProfilesTask + init { testClass.loadSimData() @@ -367,88 +369,51 @@ class SimAdministrationTest { hssAdapterProxy = hssAdapterCache, pvaf = pvaf) simApi = SimInventoryApi(httpClient, SIM_MANAGER_RULE.configuration, simDao) - } - } - - - @Test - fun testPollingOfOutstandingProfilesTask() { - loadSimData() - val simDao = SIM_MANAGER_RULE.getApplication() - .getDAO() - val profileVendors = SIM_MANAGER_RULE.configuration.profileVendors - val hssConfigs = SIM_MANAGER_RULE.configuration.hssVendors - val httpClient = HttpClientBuilder(SIM_MANAGER_RULE.environment) - .build("periodicProvisioningTaskClient") - val maxNoOfProfilesToAllocate = 10 - - val hlrs = simDao.getHssEntries() - assertThat(hlrs.isRight()).isTrue() + pollingTask = PollOutstandingProfilesTask(simInventoryDAO = simDao, pvaf = pvaf) + } - var hssId: Long = 0 - hlrs.map { - hssId = it[0].id + fun executePreallocateSimProfilesTask() { + preallocationTask.preAllocateSimProfiles() // TODO: Should be rewritten, only assume "execute" available. } - val dispatcher = DirectHssDispatcher( - hssConfigs = hssConfigs, - httpClient = httpClient, - healthCheckRegistrar = object : HealthCheckRegistrar { - override fun registerHealthCheck(name: String, healthCheck: HealthCheck) { - SIM_MANAGER_RULE.environment.healthChecks().register(name, healthCheck) - } - }) - val hssAdapterCache = SimManagerToHssDispatcherAdapter( - dispatcher = dispatcher, - simInventoryDAO = simDao) - val preStats = SimProfileKeyStatistics( - noOfEntries = 0L, - noOfEntriesAvailableForImmediateUse = 0L, - noOfReleasedEntries = 0L, - noOfUnallocatedEntries = 0L, - noOfReservedEntries = 0L) - val pvaf = ProfileVendorAdapterFactory( - simInventoryDAO = simDao, - httpClient = httpClient, - profileVendors = ConfigRegistry.config.profileVendors) - val preallocationTask = PreallocateProfilesTask( - maxNoOfProfileToAllocate = maxNoOfProfilesToAllocate, - simInventoryDAO = simDao, - hssAdapterProxy = hssAdapterCache, - pvaf = pvaf) + fun executePollOutstandingSimrofilesTask() : String { + // Then run the polling task and see what happens. + val out = StringWriter(); + val writer = PrintWriter(out); + val parameters = ImmutableMultimap.builder().build() + pollingTask.execute(parameters, writer) + return out.toString() + } - preallocationTask.preAllocateSimProfiles() // TODO: Should be rewritten, only assume "execute" available. + fun allocateNextEsimProfile ():SimEntry { + val simEntry = simApi.allocateNextEsimProfile(hssName = testClass.hssName, phoneType = "nokia") + .fold({ null }, { it }) - val pollingTask = PollOutstandingProfilesTask(simInventoryDAO = simDao, pvaf = pvaf) + assertNotNull(simEntry) + return simEntry!! + } + } - // Wrap in a method that executes the task and returns the result-string, use that - // for the preallocation task also. - val hssName = ConfigRegistry?.config.hssVendors[0].name - assertNotNull(hssName) + @Test + fun testPollingOfOutstandingProfilesTask() { + val tif = TaskInvocationFixture(this) - // Allocate the next available simcard - // val config = SIM_MANAGER_RULE.getApplication() - val simApi: SimInventoryApi = SimInventoryApi(httpClient, SIM_MANAGER_RULE.configuration, simDao) + tif.executePreallocateSimProfilesTask() - // TODO: Run the polling, and observe that nothing happens. - val simEntry = simApi.allocateNextEsimProfile(hssName = hssName, phoneType = "nokia") - .fold({ null }, { it }) + // Run the polling, and observe that nothing happens. + // Then run the polling task and see what happens. + var outString = tif.executePollOutstandingSimrofilesTask() + assertTrue(true) // TODO: Replace with something more useful - assertNotNull(simEntry) + // Then allocate the next profile + val simEntry = tif.allocateNextEsimProfile() // Then run the polling task and see what happens. - val out = StringWriter(); - val writer = PrintWriter(out); - val parameters = ImmutableMultimap.builder().build() - pollingTask.execute(parameters, writer) - - // First real verification. Check that nothing has changed so far, neither in the - // SMDP or in the database. - val outString = out.toString() - assertTrue(outString.contains("State for iccid=8901000000000000977 still set to RELEASED")) + outString = tif.executePollOutstandingSimrofilesTask() + assertTrue(outString.contains("State for iccid=${simEntry.iccid} still set to RELEASED")) // TODO: Simulate that ICCID 8901000000000000977 is downloaded, but no callback is sent! // TODO: Again check the value of the value by polling, and possibly updating the value. From 40a6f0f2ccdd348e4b5bb8bc62cf7205790327dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Remseth?= Date: Fri, 29 Nov 2019 09:28:08 +0100 Subject: [PATCH 14/22] Extending the smdp+ emulator to have callbacks disabled --- .../simcards/admin/SimAdministrationTest.kt | 2 +- .../simcards/smdpplus/SmDpPlusApplication.kt | 45 ++++++++++++++----- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt index 1ed9284a4..22bb75501 100644 --- a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt +++ b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt @@ -331,7 +331,7 @@ class SimAdministrationTest { profileVendors = SIM_MANAGER_RULE.configuration.profileVendors hssConfigs = SIM_MANAGER_RULE.configuration.hssVendors httpClient = HttpClientBuilder(SIM_MANAGER_RULE.environment) - .build("periodicProvisioningTaskClient") + .build("taskInvocationFixtureClient") maxNoOfProfilesToAllocate = 10 val hlrs = simDao.getHssEntries() diff --git a/sim-administration/sm-dp-plus-emulator/src/main/kotlin/org/ostelco/simcards/smdpplus/SmDpPlusApplication.kt b/sim-administration/sm-dp-plus-emulator/src/main/kotlin/org/ostelco/simcards/smdpplus/SmDpPlusApplication.kt index fdf6b0290..9c29b33cc 100644 --- a/sim-administration/sm-dp-plus-emulator/src/main/kotlin/org/ostelco/simcards/smdpplus/SmDpPlusApplication.kt +++ b/sim-administration/sm-dp-plus-emulator/src/main/kotlin/org/ostelco/simcards/smdpplus/SmDpPlusApplication.kt @@ -81,6 +81,18 @@ class SmDpPlusApplication : Application() { fun getHttpClient() = httpClient + lateinit var callbackClient:SmDpPlusCallbackClient + + var callbacksEnabled = true + + fun enableCallbacks() { + callbacksEnabled = true + } + + fun disableCallbacks() { + callbacksEnabled = false + } + override fun run(config: SmDpPlusAppConfiguration, env: Environment) { @@ -106,7 +118,7 @@ class SmDpPlusApplication : Application() { } val simEntriesIterator = SmDpSimEntryIterator(FileInputStream(config.simBatchData)) - val smDpPlusEmulator = SmDpPlusEmulator(simEntriesIterator) + val smDpPlusEmulator = SmDpPlusEmulator(simEntriesIterator, this) this.smdpPlusService = smDpPlusEmulator this.serverResource = SmDpPlusServerResource( @@ -118,15 +130,14 @@ class SmDpPlusApplication : Application() { certConfig = config.certConfig))) */ - val callbackClient = SmDpPlusCallbackClient( + callbackClient = SmDpPlusCallbackClient( httpClient = httpClient, - hostname = config.es2plusConfig.host, portNumber = config.es2plusConfig.port, requesterId = config.es2plusConfig.requesterId, smdpPlus = smdpPlusService) - val commandsProcessor = CommandsProcessorResource(callbackClient) + val commandsProcessor = CommandsProcessorResource(this) jerseyEnvironment.register(commandsProcessor) // XXX This is weird, is it even necessary? Probably not. @@ -148,6 +159,16 @@ class SmDpPlusApplication : Application() { fun reset() { this.smdpPlusService.reset(); } + + fun setCurrentState(iccid: String, newState: String) { + if (newState == "DOWNLOAD" && callbacksEnabled) { // TODO: Add flag to switch reporting off (to test error situations) + callbackClient.reportDownload(iccid) + } + } + + fun reportDownload(iccid: String) { + setCurrentState(iccid, "DOWNLOAD") + } } @@ -194,13 +215,12 @@ class SmDpPlusCallbackClient( * that has been registred to receive the callbacks. */ @Path("commands") -class CommandsProcessorResource(private val callbackClient: SmDpPlusCallbackClient) { - +class CommandsProcessorResource(private val app: SmDpPlusApplication) { @Path("simulate-download-of/iccid/{iccid}") @GET fun simulateDownloadOf(@PathParam("iccid") iccid: String): String { - callbackClient.reportDownload(iccid = iccid) + app.reportDownload(iccid) return "Simulated download of iccid ${iccid} went well." } } @@ -210,7 +230,7 @@ class CommandsProcessorResource(private val callbackClient: SmDpPlusCallbackClie * happy day scenarios, and not particulary efficient, and in-memory * only etc. */ -class SmDpPlusEmulator(incomingEntries: Iterator) : SmDpPlusService { +class SmDpPlusEmulator(incomingEntries: Iterator, val app: SmDpPlusApplication) : SmDpPlusService { private val log = LoggerFactory.getLogger(javaClass) @@ -285,6 +305,11 @@ class SmDpPlusEmulator(incomingEntries: Iterator) : SmDpPlusServic fun getEntryByIccid(iccid: String): SmDpSimEntry? = entriesByIccid[iccid] + fun setEntryState(entry: SmDpSimEntry, newState: String) { + entry.setCurrentState(newState) + app.setCurrentState(entry.iccid, newState) + } + // TODO; What about the reservation flag? override fun downloadOrder(eid: String?, iccid: String?, profileType: String?): Es2DownloadOrderResponse { @@ -300,7 +325,7 @@ class SmDpPlusEmulator(incomingEntries: Iterator) : SmDpPlusServic // Then mark the entry as allocated and return the corresponding ICCID. entry.allocated = true - entry.setCurrentState("DOWNLOADED") + setEntryState(entry, "DOWNLOADED") // Finally return the ICCID uniquely identifying the profile instance. return Es2DownloadOrderResponse(eS2SuccessResponseHeader(), @@ -390,7 +415,7 @@ class SmDpPlusEmulator(incomingEntries: Iterator) : SmDpPlusServic // XXX The state mechanism in this class is a pice of .... Fix it! entry.released = releaseFlag - entry.setCurrentState("RELEASED") + setEntryState(entry, "RELEASED") if (confirmationCode != null) { From bc5add58e573a1c5bf588d9bf6c400fb25bae149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Remseth?= Date: Fri, 29 Nov 2019 09:55:01 +0100 Subject: [PATCH 15/22] Passing test, now testing actual workitude --- .../simcards/admin/SimAdministrationTest.kt | 19 +++++++++++++------ .../admin/PollOutstandingProfilesTask.kt | 2 +- .../simcards/smdpplus/SmDpPlusApplication.kt | 15 +++++++++++++++ 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt index 22bb75501..8e3a01703 100644 --- a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt +++ b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt @@ -393,6 +393,14 @@ class SimAdministrationTest { assertNotNull(simEntry) return simEntry!! } + + fun disableEs2CallbacksFromSmdpPlus() { + SM_DP_PLUS_RULE.getApplication().disableCallbacks() + } + + fun emulateDownloadOfIccid(iccid: String) { + SM_DP_PLUS_RULE.getApplication().emulateInstallOfIccid(iccid) + } } @@ -402,7 +410,6 @@ class SimAdministrationTest { tif.executePreallocateSimProfilesTask() - // Run the polling, and observe that nothing happens. // Then run the polling task and see what happens. var outString = tif.executePollOutstandingSimrofilesTask() @@ -415,10 +422,11 @@ class SimAdministrationTest { outString = tif.executePollOutstandingSimrofilesTask() assertTrue(outString.contains("State for iccid=${simEntry.iccid} still set to RELEASED")) - // TODO: Simulate that ICCID 8901000000000000977 is downloaded, but no callback is sent! - // TODO: Again check the value of the value by polling, and possibly updating the value. - // TODO: Again, check that the test ran, but nothing happened. - // TODO: Perhaps, make the verifications a bit less brittle than grepping in a string? + tif.disableEs2CallbacksFromSmdpPlus() + tif.emulateDownloadOfIccid(simEntry.iccid) + + outString = tif.executePollOutstandingSimrofilesTask() + assertTrue(outString.contains("Updated state for iccid=${simEntry.iccid} to INSTALLED")) } @@ -613,7 +621,6 @@ class SimAdministrationTest { assertGaugeValue(2, "sims.noOfReservedEntries.IPHONE_PROFILE_2") } - // XXX MISSING TEST: SHould test periodic updater also in cases where // either HSS or SM-DP+ entries are pre-allocated. diff --git a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PollOutstandingProfilesTask.kt b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PollOutstandingProfilesTask.kt index bd30ba65f..a63b3f895 100644 --- a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PollOutstandingProfilesTask.kt +++ b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PollOutstandingProfilesTask.kt @@ -92,7 +92,7 @@ class PollOutstandingProfilesTask( // This probably represents an error situation in the SM-DP+, and _should_ perhaps // be reported as an error by the sim manager. Please think about this and // either change it to logger.error yourself, or ask me in review comments to do it for you. - val report = "Updated state for iccid=$iccid still set to ${state.name}" + val report = "Updated state for iccid=$iccid to ${state.name}" logger.info(report) reportln(report) } diff --git a/sim-administration/sm-dp-plus-emulator/src/main/kotlin/org/ostelco/simcards/smdpplus/SmDpPlusApplication.kt b/sim-administration/sm-dp-plus-emulator/src/main/kotlin/org/ostelco/simcards/smdpplus/SmDpPlusApplication.kt index 9c29b33cc..9cff14cfa 100644 --- a/sim-administration/sm-dp-plus-emulator/src/main/kotlin/org/ostelco/simcards/smdpplus/SmDpPlusApplication.kt +++ b/sim-administration/sm-dp-plus-emulator/src/main/kotlin/org/ostelco/simcards/smdpplus/SmDpPlusApplication.kt @@ -158,6 +158,7 @@ class SmDpPlusApplication : Application() { fun reset() { this.smdpPlusService.reset(); + enableCallbacks() } fun setCurrentState(iccid: String, newState: String) { @@ -169,6 +170,10 @@ class SmDpPlusApplication : Application() { fun reportDownload(iccid: String) { setCurrentState(iccid, "DOWNLOAD") } + + fun emulateInstallOfIccid(iccid: String) { + smdpPlusService.emulateInstallOfIccid(iccid) + } } @@ -279,6 +284,7 @@ class SmDpPlusEmulator(incomingEntries: Iterator, val app: SmDpPlu fun reset() { + app.enableCallbacks() entries.clear() entriesByIccid.clear() entriesByProfile.clear() @@ -310,6 +316,11 @@ class SmDpPlusEmulator(incomingEntries: Iterator, val app: SmDpPlu app.setCurrentState(entry.iccid, newState) } + fun setEntryStateByIccid(iccid: String, state: String) { + val entry: SmDpSimEntry = getEntryByIccid(iccid) + ?: throw SmDpPlusException("Could not find download order matching criteria") + setEntryState(entry, state) + } // TODO; What about the reservation flag? override fun downloadOrder(eid: String?, iccid: String?, profileType: String?): Es2DownloadOrderResponse { @@ -458,6 +469,10 @@ class SmDpPlusEmulator(incomingEntries: Iterator, val app: SmDpPlu override fun releaseProfile(iccid: String) { TODO("not implemented") } + + fun emulateInstallOfIccid(iccid: String) { + setEntryStateByIccid(iccid, "INSTALLED") + } } /** From deab9bb4f777a5ff0fd8e53f69deb6a51e916215 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Remseth?= Date: Fri, 29 Nov 2019 09:56:49 +0100 Subject: [PATCH 16/22] Adding comment --- .../kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt index 8e3a01703..9342eca10 100644 --- a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt +++ b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt @@ -422,6 +422,8 @@ class SimAdministrationTest { outString = tif.executePollOutstandingSimrofilesTask() assertTrue(outString.contains("State for iccid=${simEntry.iccid} still set to RELEASED")) + // Disable callbacks and emulate dowload (emulation of failing callbacks from SMDP+, which + // is the situation we want the callbacks to help us with) tif.disableEs2CallbacksFromSmdpPlus() tif.emulateDownloadOfIccid(simEntry.iccid) From 315a17d53c4272b3b22c3f9f1b7f6274f8b2eeda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Remseth?= Date: Fri, 29 Nov 2019 10:11:10 +0100 Subject: [PATCH 17/22] More refactoring for compactness and readability --- .../simcards/admin/SimAdministrationTest.kt | 82 ++++++------------- 1 file changed, 26 insertions(+), 56 deletions(-) diff --git a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt index 9342eca10..8af0c9c03 100644 --- a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt +++ b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt @@ -46,6 +46,7 @@ import java.io.PrintWriter import java.io.StringWriter import java.time.Duration import java.time.temporal.ChronoUnit +import java.util.* import javax.ws.rs.client.Client import javax.ws.rs.client.Entity import javax.ws.rs.core.MediaType @@ -322,6 +323,8 @@ class SimAdministrationTest { val pollingTask : PollOutstandingProfilesTask + var hssId: Long = -1L + init { testClass.loadSimData() @@ -331,13 +334,11 @@ class SimAdministrationTest { profileVendors = SIM_MANAGER_RULE.configuration.profileVendors hssConfigs = SIM_MANAGER_RULE.configuration.hssVendors httpClient = HttpClientBuilder(SIM_MANAGER_RULE.environment) - .build("taskInvocationFixtureClient") - maxNoOfProfilesToAllocate = 10 + .build("taskInvocationFixtureClient-${UUID.randomUUID()}") val hlrs = simDao.getHssEntries() assertThat(hlrs.isRight()).isTrue() - var hssId: Long = 0 hlrs.map { hssId = it[0].id } @@ -401,6 +402,23 @@ class SimAdministrationTest { fun emulateDownloadOfIccid(iccid: String) { SM_DP_PLUS_RULE.getApplication().emulateInstallOfIccid(iccid) } + + + fun getStatsForProfile(profile:String): SimProfileKeyStatistics { + val postAllocationStats = + simDao.getProfileStats(hssId, profile) + assertThat(postAllocationStats.isRight()).isTrue() + + var stats : SimProfileKeyStatistics? = null + postAllocationStats.map { + stats = it + } + if (stats == null) { + fail("Failed to get stats for profile $profile") + } + + return stats!! + } } @@ -434,65 +452,17 @@ class SimAdministrationTest { @Test fun testPeriodicProvisioningTask() { - loadSimData() - val simDao = SIM_MANAGER_RULE.getApplication() - .getDAO() - - val profileVendors = SIM_MANAGER_RULE.configuration.profileVendors - val hssConfigs = SIM_MANAGER_RULE.configuration.hssVendors - val httpClient = HttpClientBuilder(SIM_MANAGER_RULE.environment) - .build("periodicProvisioningTaskClient") - val maxNoOfProfilesToAllocate = 10 - - val hlrs = simDao.getHssEntries() - assertThat(hlrs.isRight()).isTrue() + val tif = TaskInvocationFixture(this) - var hssId: Long = 0 - hlrs.map { - hssId = it[0].id - } + val preStats = tif.getStatsForProfile(expectedProfile) + tif.executePreallocateSimProfilesTask() - val dispatcher = DirectHssDispatcher( - hssConfigs = hssConfigs, - httpClient = httpClient, - healthCheckRegistrar = object : HealthCheckRegistrar { - override fun registerHealthCheck(name: String, healthCheck: HealthCheck) { - SIM_MANAGER_RULE.environment.healthChecks().register(name, healthCheck) - } - }) - val hssAdapterCache = SimManagerToHssDispatcherAdapter( - dispatcher = dispatcher, - simInventoryDAO = simDao) - val preStats = SimProfileKeyStatistics( - noOfEntries = 0L, - noOfEntriesAvailableForImmediateUse = 0L, - noOfReleasedEntries = 0L, - noOfUnallocatedEntries = 0L, - noOfReservedEntries = 0L) - val pvaf = ProfileVendorAdapterFactory( - simInventoryDAO = simDao, - httpClient = httpClient, - profileVendors = ConfigRegistry.config.profileVendors) - val task = PreallocateProfilesTask( - maxNoOfProfileToAllocate = maxNoOfProfilesToAllocate, - simInventoryDAO = simDao, - hssAdapterProxy = hssAdapterCache, - pvaf = pvaf) - task.preAllocateSimProfiles() - - val postAllocationStats = - simDao.getProfileStats(hssId, expectedProfile) - assertThat(postAllocationStats.isRight()).isTrue() - - var postStats = SimProfileKeyStatistics(0L, 0L, 0L, 0L, 0L) - postAllocationStats.map { - postStats = it - } + val postStats = tif.getStatsForProfile(expectedProfile) val noOfAllocatedProfiles = postStats.noOfEntriesAvailableForImmediateUse - preStats.noOfEntriesAvailableForImmediateUse assertEquals( - maxNoOfProfilesToAllocate.toLong(), + tif.maxNoOfProfilesToAllocate.toLong(), noOfAllocatedProfiles) } From 5a009673e74c259ca12331a3a03e5f19f8d0d1d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Remseth?= Date: Fri, 29 Nov 2019 11:02:44 +0100 Subject: [PATCH 18/22] A little more abstraction --- .../simcards/admin/SimAdministrationTest.kt | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt index 8af0c9c03..6bd6aac4a 100644 --- a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt +++ b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt @@ -374,19 +374,24 @@ class SimAdministrationTest { pollingTask = PollOutstandingProfilesTask(simInventoryDAO = simDao, pvaf = pvaf) } - fun executePreallocateSimProfilesTask() { - preallocationTask.preAllocateSimProfiles() // TODO: Should be rewritten, only assume "execute" available. - } + // typealias TaskExecutionFunction = (ImmutableMultimap, PrintWriter) -> Unit - fun executePollOutstandingSimrofilesTask() : String { - // Then run the polling task and see what happens. + fun executeTask(xf: (ImmutableMultimap, PrintWriter) -> Unit): String { val out = StringWriter(); val writer = PrintWriter(out); val parameters = ImmutableMultimap.builder().build() - pollingTask.execute(parameters, writer) + xf(parameters, writer) return out.toString() } + fun executePreallocateSimProfilesTask(): String { + return executeTask(preallocationTask::execute) + } + + fun executePollOutstandingSimrofilesTask() : String { + return executeTask(pollingTask::execute) + } + fun allocateNextEsimProfile ():SimEntry { val simEntry = simApi.allocateNextEsimProfile(hssName = testClass.hssName, phoneType = "nokia") .fold({ null }, { it }) @@ -449,7 +454,6 @@ class SimAdministrationTest { assertTrue(outString.contains("Updated state for iccid=${simEntry.iccid} to INSTALLED")) } - @Test fun testPeriodicProvisioningTask() { val tif = TaskInvocationFixture(this) From 84a3830cf591c496fd967462447190765517988d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Remseth?= Date: Fri, 29 Nov 2019 11:07:50 +0100 Subject: [PATCH 19/22] Simplify even more, jsut use the original task type and apply the execute function in it. --- .../ostelco/simcards/admin/SimAdministrationTest.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt index 6bd6aac4a..0de4f9bb5 100644 --- a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt +++ b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt @@ -9,6 +9,7 @@ import com.google.gson.JsonParser import io.dropwizard.client.HttpClientBuilder import io.dropwizard.client.JerseyClientBuilder import io.dropwizard.jdbi3.JdbiFactory +import io.dropwizard.servlets.tasks.Task import io.dropwizard.testing.ConfigOverride import io.dropwizard.testing.ResourceHelpers import io.dropwizard.testing.junit.DropwizardAppRule @@ -374,22 +375,21 @@ class SimAdministrationTest { pollingTask = PollOutstandingProfilesTask(simInventoryDAO = simDao, pvaf = pvaf) } - // typealias TaskExecutionFunction = (ImmutableMultimap, PrintWriter) -> Unit - fun executeTask(xf: (ImmutableMultimap, PrintWriter) -> Unit): String { + fun executeTask(task: Task): String { val out = StringWriter(); val writer = PrintWriter(out); val parameters = ImmutableMultimap.builder().build() - xf(parameters, writer) + task.execute(parameters, writer) return out.toString() } fun executePreallocateSimProfilesTask(): String { - return executeTask(preallocationTask::execute) + return executeTask(preallocationTask) } fun executePollOutstandingSimrofilesTask() : String { - return executeTask(pollingTask::execute) + return executeTask(pollingTask) } fun allocateNextEsimProfile ():SimEntry { From 6f6378261ca0afd73778bea58f5b18a40c7a923b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Remseth?= Date: Fri, 29 Nov 2019 16:08:50 +0100 Subject: [PATCH 20/22] Adding a bunch of comments --- .../org/ostelco/sim/es2plus/Es2PlusClient.kt | 10 ++++--- .../simcards/admin/SimAdministrationTest.kt | 28 ++++++++++++++++++- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/sim-administration/es2plus4dropwizard/src/main/kotlin/org/ostelco/sim/es2plus/Es2PlusClient.kt b/sim-administration/es2plus4dropwizard/src/main/kotlin/org/ostelco/sim/es2plus/Es2PlusClient.kt index 532871fc8..43d0a47ad 100644 --- a/sim-administration/es2plus4dropwizard/src/main/kotlin/org/ostelco/sim/es2plus/Es2PlusClient.kt +++ b/sim-administration/es2plus4dropwizard/src/main/kotlin/org/ostelco/sim/es2plus/Es2PlusClient.kt @@ -119,9 +119,9 @@ class ES2PlusClient( return response } - /** - * For test cases where content should be returned. - */ + // + // For test cases where content should be returned. + // @Throws(ES2PlusClientException::class) private fun postEs2ProtocolCmd( path: String, @@ -233,7 +233,6 @@ class ES2PlusClient( } - fun confirmOrder(eid: String? = null, iccid: String, matchingId: String? = null, @@ -257,6 +256,9 @@ class ES2PlusClient( returnValueClass = Es2ConfirmOrderResponse::class.java) } + /** + * Transmit a cancelOrder request for a particular ICCID. + */ fun cancelOrder(iccid: String, finalProfileStatusIndicator: String, eid: String? = null, diff --git a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt index 0de4f9bb5..d11c41bd6 100644 --- a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt +++ b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt @@ -308,6 +308,10 @@ class SimAdministrationTest { } } + /** + * Helper class that is used to wrap invocations of the + * preallocation and polling of outstandng profiles tasks. + */ class TaskInvocationFixture(val testClass: SimAdministrationTest) { var simDao: SimInventoryDAO var profileVendors: List @@ -376,7 +380,8 @@ class SimAdministrationTest { } - fun executeTask(task: Task): String { + + private fun executeTask(task: Task): String { val out = StringWriter(); val writer = PrintWriter(out); val parameters = ImmutableMultimap.builder().build() @@ -384,14 +389,25 @@ class SimAdministrationTest { return out.toString() } + /** + * Execute the preallocation task, returning the report it makes as a string. + */ fun executePreallocateSimProfilesTask(): String { return executeTask(preallocationTask) } + /** + * Execute the polling of outstanding sim roles task, returning the report it makes as a string. + */ fun executePollOutstandingSimrofilesTask() : String { return executeTask(pollingTask) } + /** + * Allocate the next sim profile for a generic ("nokia") phone. This method + * doesn't actually use the tasks, but it is used as a convenience methods when + * testing the tasks. + */ fun allocateNextEsimProfile ():SimEntry { val simEntry = simApi.allocateNextEsimProfile(hssName = testClass.hssName, phoneType = "nokia") .fold({ null }, { it }) @@ -400,15 +416,25 @@ class SimAdministrationTest { return simEntry!! } + /** + * Switch off callbacks from the SM-DP+ emulator to the Prime emulator. + */ fun disableEs2CallbacksFromSmdpPlus() { SM_DP_PLUS_RULE.getApplication().disableCallbacks() } + /** + * Instruct the SM-DP+ emulator to simulate an installation of a simcard. + */ fun emulateDownloadOfIccid(iccid: String) { SM_DP_PLUS_RULE.getApplication().emulateInstallOfIccid(iccid) } + /** + * Get statistics for a specific type of sim profile. Used to test + * if the preallocation task is doing the right thing. + */ fun getStatsForProfile(profile:String): SimProfileKeyStatistics { val postAllocationStats = simDao.getProfileStats(hssId, profile) From 1d1ee0aff8e2230bbfd77850e81e318f021a97cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Remseth?= Date: Fri, 29 Nov 2019 16:09:20 +0100 Subject: [PATCH 21/22] Whitespace --- .../simcards/admin/PollOutstandingProfilesTask.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PollOutstandingProfilesTask.kt b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PollOutstandingProfilesTask.kt index a63b3f895..ccd9b4178 100644 --- a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PollOutstandingProfilesTask.kt +++ b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PollOutstandingProfilesTask.kt @@ -105,10 +105,8 @@ class PollOutstandingProfilesTask( } fun asVendorIdToIccdList(profiles: List): Map> { - val iccidsByProfileVendorIdMap : MutableMap> - = mutableMapOf>() - profiles.forEach { - simEntry: SimEntry -> + val iccidsByProfileVendorIdMap: MutableMap> = mutableMapOf>() + profiles.forEach { simEntry: SimEntry -> val vendorId = simEntry.profileVendorId if (!iccidsByProfileVendorIdMap.containsKey(vendorId)) { iccidsByProfileVendorIdMap[vendorId] = mutableListOf() @@ -125,8 +123,9 @@ class PollOutstandingProfilesTask( for ((vendorId, iccidList) in asVendorIdToIccdList(profilesToPoll)) { pvaf.getAdapterByVendorId(vendorId).mapRight { profileVendorAdapter -> val statuses = profileVendorAdapter.getProfileStatusList(iccidList) - statuses.mapRight{ - it.forEach { updateProfileInDb(it) } } + statuses.mapRight { + it.forEach { updateProfileInDb(it) } + } } // TODO: Am I missing the error situation here? } } From b22b39cdbf21e49b1ce07bb132b7a7fcce4b9de1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Remseth?= Date: Thu, 12 Dec 2019 10:19:29 +0100 Subject: [PATCH 22/22] Adding some of Kjell's excellent suggestions from code review --- .../admin/PollOutstandingProfilesTask.kt | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PollOutstandingProfilesTask.kt b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PollOutstandingProfilesTask.kt index ccd9b4178..b2dae1737 100644 --- a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PollOutstandingProfilesTask.kt +++ b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PollOutstandingProfilesTask.kt @@ -104,17 +104,18 @@ class PollOutstandingProfilesTask( } } - fun asVendorIdToIccdList(profiles: List): Map> { - val iccidsByProfileVendorIdMap: MutableMap> = mutableMapOf>() - profiles.forEach { simEntry: SimEntry -> - val vendorId = simEntry.profileVendorId - if (!iccidsByProfileVendorIdMap.containsKey(vendorId)) { - iccidsByProfileVendorIdMap[vendorId] = mutableListOf() + + fun asVendorIdToIccdList(profiles: List): Map> = + profiles.map { + mapOf(it.profileVendorId to it.iccid) + }.flatMap { + it.entries + }.groupBy { + it.key + }.mapValues { vendor -> + vendor.value.map { it.value } } - iccidsByProfileVendorIdMap[vendorId]?.add(simEntry.iccid) - } - return iccidsByProfileVendorIdMap - } + // Then poll them individually via the profile vendor adapter associated with the // profile (there is not much to be gained here by doing it in paralellel, but perhaps @@ -122,7 +123,8 @@ class PollOutstandingProfilesTask( for ((vendorId, iccidList) in asVendorIdToIccdList(profilesToPoll)) { pvaf.getAdapterByVendorId(vendorId).mapRight { profileVendorAdapter -> - val statuses = profileVendorAdapter.getProfileStatusList(iccidList) + val statuses = + profileVendorAdapter.getProfileStatusList(iccidList) statuses.mapRight { it.forEach { updateProfileInDb(it) } }