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
66 changes: 66 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: CI

on:
pull_request:
push:
branches:
- main

jobs:
unit-tests:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3

- name: Build and run unit tests
run: ./gradlew build test

integration-tests:
runs-on: ubuntu-latest
needs: unit-tests

steps:
- name: Check for required secrets
id: check-secrets
run: |
if [ -z "${{ secrets.INSFORGE_BASE_URL }}" ]; then
echo "available=false" >> "$GITHUB_OUTPUT"
echo "Secrets not available — skipping integration tests (fork PR)"
else
echo "available=true" >> "$GITHUB_OUTPUT"
fi

- name: Checkout repository
if: steps.check-secrets.outputs.available == 'true'
uses: actions/checkout@v4

- name: Set up JDK 17
if: steps.check-secrets.outputs.available == 'true'
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Setup Gradle
if: steps.check-secrets.outputs.available == 'true'
uses: gradle/actions/setup-gradle@v3

- name: Run integration tests
if: steps.check-secrets.outputs.available == 'true'
run: ./gradlew integrationTest
env:
INSFORGE_BASE_URL: ${{ secrets.INSFORGE_BASE_URL }}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is indeed the case that secrets are unavailable for PRs originating from forks. Consequently, the current job still executes, passing empty strings to the integration tests.
Could you modify the workflow to first check for the presence of secrets; if they are missing, the integration tests will be skipped entirely to prevent fork PRs from failing due to missing configuration.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

working on it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed now, please have a look

INSFORGE_ANON_KEY: ${{ secrets.INSFORGE_ANON_KEY }}
INSFORGE_TEST_EMAIL: ${{ secrets.INSFORGE_TEST_EMAIL }}
INSFORGE_TEST_PASSWORD: ${{ secrets.INSFORGE_TEST_PASSWORD }}
8 changes: 4 additions & 4 deletions .github/workflows/publish-maven-central.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up JDK 11
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '11'
java-version: '17'
distribution: 'temurin'

- name: Setup Gradle
Expand All @@ -38,10 +38,10 @@ jobs:
sed -i "s/version = \".*\"/version = \"$VERSION\"/" build.gradle.kts

- name: Build
run: ./gradlew clean build -x test
run: ./gradlew clean build

- name: Publish to Maven Central
run: ./gradlew publishAndReleaseToMavenCentral -x test
run: ./gradlew publishAndReleaseToMavenCentral
env:
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
Expand Down
11 changes: 7 additions & 4 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up JDK 11
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '11'
java-version: '17'
distribution: 'temurin'

- name: Setup Gradle
Expand All @@ -35,8 +35,11 @@ jobs:
run: |
sed -i "s/version = \".*\"/version = \"${{ github.event.inputs.version }}\"/" build.gradle.kts

- name: Build and Publish to GitHub Packages
run: ./gradlew clean publishAllPublicationsToGitHubPackagesRepository -x test
- name: Build
run: ./gradlew clean build

- name: Publish to GitHub Packages
run: ./gradlew publishAllPublicationsToGitHubPackagesRepository
env:
GITHUB_ACTOR: ${{ github.actor }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,12 @@ src/main/kotlin/dev/insforge/
# Build
./gradlew clean build

# Run tests
# Run unit tests (CI-safe, no external service dependency)
./gradlew test

# Run integration tests (requires InsForge test backend)
./gradlew integrationTest

# Publish to local Maven
./gradlew publishToMavenLocal
```
Expand Down
33 changes: 30 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ dependencies {
implementation("io.socket:socket.io-client:2.1.1")

// Testing
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
testImplementation("io.ktor:ktor-client-mock:2.3.7")
}
Expand Down Expand Up @@ -110,15 +110,42 @@ tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
}
}

tasks.withType<Test> {
useJUnitPlatform()
tasks.withType<Test>().configureEach {
useJUnitPlatform {
// `test` is our unit-test task in CI; integration tests run via `integrationTest`.
if (name == "test") {
excludeTags("integration")
}
}

// Show test output including HTTP logs
testLogging {
showStandardStreams = true
events("passed", "skipped", "failed")
}
}

val integrationTest by tasks.registering(Test::class) {
description = "Runs integration tests that depend on external services"
group = LifecycleBasePlugin.VERIFICATION_GROUP

testClassesDirs = sourceSets["test"].output.classesDirs
classpath = sourceSets["test"].runtimeClasspath

maxHeapSize = "512m"

useJUnitPlatform {
includeTags("integration")
}

testLogging {
showStandardStreams = true
events("passed", "skipped", "failed")
}

shouldRunAfter(tasks.named("test"))
}

// Ensure all Jar tasks depend on generateVersionFile
tasks.withType<Jar>().configureEach {
dependsOn(generateVersionFile)
Expand Down
6 changes: 4 additions & 2 deletions src/test/kotlin/dev/insforge/ApiResponseTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Tag
import kotlin.test.Test

/**
* Test to capture raw API responses for model adjustment
*/
@Tag("integration")
class ApiResponseTest {

private val baseURL = "https://pg6afqz9.us-east.insforge.app"
private val anonKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3OC0xMjM0LTU2NzgtOTBhYi1jZGVmMTIzNDU2NzgiLCJlbWFpbCI6ImFub25AaW5zZm9yZ2UuY29tIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Njc5MDc5MzJ9.K0semVtcacV55qeEhVUI3WKWzT7p87JU7wNzdXysRWo"
private val baseURL = TestConfig.BASE_URL
private val anonKey = TestConfig.ANON_KEY

private val httpClient = HttpClient(OkHttp) {
expectSuccess = false
Expand Down
6 changes: 4 additions & 2 deletions src/test/kotlin/dev/insforge/CaptureApiResponsesTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Tag
import kotlin.test.Test

/**
* Test to capture raw API responses for model adjustment
*/
@Tag("integration")
class CaptureApiResponsesTest {

private val baseURL = "https://pg6afqz9.us-east.insforge.app"
private val anonKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3OC0xMjM0LTU2NzgtOTBhYi1jZGVmMTIzNDU2NzgiLCJlbWFpbCI6ImFub25AaW5zZm9yZ2UuY29tIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Njc5MDc5MzJ9.K0semVtcacV55qeEhVUI3WKWzT7p87JU7wNzdXysRWo"
private val baseURL = TestConfig.BASE_URL
private val anonKey = TestConfig.ANON_KEY

private val httpClient = HttpClient(OkHttp) {
expectSuccess = false
Expand Down
90 changes: 68 additions & 22 deletions src/test/kotlin/dev/insforge/TestConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,67 @@ import dev.insforge.database.Database
import dev.insforge.functions.Functions
import dev.insforge.realtime.Realtime
import dev.insforge.storage.Storage
import dev.insforge.auth.auth
import dev.insforge.logging.InsforgeLogLevel

/**
* Test configuration for Insforge SDK integration tests
* Test configuration for Insforge SDK integration tests.
*
* Values are read from environment variables when available (CI),
* falling back to hardcoded defaults for local development.
*
* Environment variables:
* - INSFORGE_BASE_URL
* - INSFORGE_ANON_KEY
* - INSFORGE_TEST_EMAIL
* - INSFORGE_TEST_PASSWORD
*/
object TestConfig {
const val BASE_URL = "https://pg6afqz9.us-east.insforge.app"
const val ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3OC0xMjM0LTU2NzgtOTBhYi1jZGVmMTIzNDU2NzgiLCJlbWFpbCI6ImFub25AaW5zZm9yZ2UuY29tIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Njc5MDc5MzJ9.K0semVtcacV55qeEhVUI3WKWzT7p87JU7wNzdXysRWo"
//const val BASE_URL = "http://localhost:7130"
//const val ANON_KEY = "ik_0322f7447cd878f2e419dc8900fe3e5e"
val BASE_URL: String = System.getenv("INSFORGE_BASE_URL")
?: "https://pg6afqz9.us-east.insforge.app"

val ANON_KEY: String = System.getenv("INSFORGE_ANON_KEY")
?: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3OC0xMjM0LTU2NzgtOTBhYi1jZGVmMTIzNDU2NzgiLCJlbWFpbCI6ImFub25AaW5zZm9yZ2UuY29tIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Njc5MDc5MzJ9.K0semVtcacV55qeEhVUI3WKWzT7p87JU7wNzdXysRWo"

val TEST_EMAIL: String = System.getenv("INSFORGE_TEST_EMAIL")
?: "ci-test@insforge.dev"

val TEST_PASSWORD: String = System.getenv("INSFORGE_TEST_PASSWORD")
?: "CiTest123456!"

private var cachedAccessToken: String? = null
private var cachedUserId: String? = null

/**
* Sign in with test credentials and cache the access token and user ID.
* Attempts sign-up first (no-op if the user already exists).
*/
private suspend fun ensureSignedIn() {
if (cachedAccessToken != null) return

val authClient = createAuthClient()
try {
try {
authClient.auth.signUp(email = TEST_EMAIL, password = TEST_PASSWORD, name = "CI Test User")
} catch (_: Exception) { }

val response = authClient.auth.signIn(email = TEST_EMAIL, password = TEST_PASSWORD)
cachedAccessToken = response.accessToken
cachedUserId = response.user.id
} finally {
authClient.close()
}
}

suspend fun getAccessToken(): String {
ensureSignedIn()
return cachedAccessToken!!
}

suspend fun getUserId(): String {
ensureSignedIn()
return cachedUserId!!
}

/**
* Create a fully configured test client with all plugins installed
Expand Down Expand Up @@ -68,8 +119,7 @@ object TestConfig {
baseURL = BASE_URL,
anonKey = ANON_KEY
) {
// Enable full HTTP logging for debugging
logLevel = InsforgeLogLevel.VERBOSE
logLevel = InsforgeLogLevel.INFO
install(Storage)
}
}
Expand Down Expand Up @@ -112,41 +162,37 @@ object TestConfig {
}
}

// JWT token for authenticated user testing
const val TEST_JWT_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIwODVhNDgxZS05NGI4LTRiZjktYjNhMC03ZjBlNTBmN2EwNzIiLCJlbWFpbCI6Imp1bndlbi5mZW5nQGluc2ZvcmdlLmRldiIsInJvbGUiOiJhdXRoZW50aWNhdGVkIiwiaWF0IjoxNzY3NzQyMTE2LCJleHAiOjE3NjgzNDY5MTZ9.jhfprod2CU1Bn2j92wG9_j0MdmbtycpRI0SHoqqDtcc"

/**
* Create a test client with JWT token for authenticated realtime testing
* Create a test client with a dynamically obtained JWT for authenticated realtime testing.
* Signs in via auth#signIn to get a fresh token instead of relying on a static JWT.
*/
fun createAuthenticatedRealtimeClient(): InsforgeClient {
suspend fun createAuthenticatedRealtimeClient(): InsforgeClient {
val token = getAccessToken()
return createInsforgeClient(
baseURL = BASE_URL,
anonKey = ANON_KEY
) {
// Use JWT token for authentication
accessToken = { TEST_JWT_TOKEN }
// Realtime needs Database for the todos tests
accessToken = { token }
install(Database)
install(Realtime) {
debug = true // Enable WebSocket message logging
debug = true
}
}
}

/**
* Create a test client with JWT token and debug logging enabled for realtime testing
* Create a test client with a dynamically obtained JWT and debug logging for realtime testing.
*/
fun createAuthenticatedRealtimeClientWithDebug(): InsforgeClient {
suspend fun createAuthenticatedRealtimeClient(debug: Boolean): InsforgeClient {
val token = getAccessToken()
return createInsforgeClient(
baseURL = BASE_URL,
anonKey = ANON_KEY
) {
// Use JWT token for authentication
accessToken = { TEST_JWT_TOKEN }
// Realtime needs Database for the todos tests
accessToken = { token }
install(Database)
install(Realtime) {
debug = true // Enable WebSocket message logging
this.debug = debug
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/test/kotlin/dev/insforge/ai/AITest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.*
import org.junit.jupiter.api.Tag
import kotlin.test.*
import kotlin.time.Duration.Companion.seconds

/**
* Integration tests for AI module
*/
@Tag("integration")
class AITest {

private lateinit var client: dev.insforge.InsforgeClient
Expand Down
Loading
Loading