Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
PhilippeBoisney committed Dec 25, 2018
0 parents commit 7eac46b
Show file tree
Hide file tree
Showing 102 changed files with 2,417 additions and 0 deletions.
Binary file added .DS_Store
Binary file not shown.
17 changes: 17 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.idea
*.iml
build
target

.gradle
local.properties
*.lock
/app/snippets-api
/backend/out
NoBullshit/app/google-services.json

.idea/modules/nobullshit.iml

.idea/vcs.xml

android/app/google-services.json
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# No Bullshit - Perfect jobs only

No Bullshit is an open source project that wants to help developers **find the perfect job**. An expert developer reviews each job submitted through the platform. We publish only ***the best***... 😎

This project is intented to show how to create a **full project** using Kotlin. Therefore, you'll find two main modules :

- **backend** : Contains the [Ktor](https://ktor.io/) backend configured to run on [Google App Engine](https://cloud.google.com/appengine/?hl=fr). It also uses [Freemarker](https://freemarker.apache.org/) for Java/html template. Data are persisted in [Firestore](https://cloud.google.com/firestore/).
- **android** : Contains the Android app written with Kotlin.

# Demo
Because a picture is worth a thousand words :
- 🌏The backend : [https://www.nobullshit.io/](https://www.nobullshit.io/)
- 📱The mobile app : #

# Where to start ?
You want to contribute or understand what this is all about, but you don't know where to start? Here are some useful resources :

**About the backend** :
- [Ktor Quickstart](https://ktor.io/quickstart/index.html)
- [Ktor on Google Cloud Appengine Standard](https://cloud.google.com/community/tutorials/kotlin-ktor-app-engine-java8)
- [Ktor Samples](https://github.com/ktorio/ktor-samples) ❤️
- [Firestore](https://cloud.google.com/firestore/docs/)

# Running samples
The backend sample can be run **locally** ([http://localhost:8080/](http://localhost:8080/)) using the following script :

./run-locally.sh
If you want to deploy it to **your own GAE project**, you can use the following script :


./deploy.sh YOUR_GAE_PROJECT_ID
# Running tests
If you want to run the tests :

./gradlew test
11 changes: 11 additions & 0 deletions android/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
*.iml
.gradle
/local.properties
/.idea/caches/build_file_checksums.ser
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
.DS_Store
/build
/captures
.externalNativeBuild
1 change: 1 addition & 0 deletions android/app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
70 changes: 70 additions & 0 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: "kotlin-allopen"

android {
compileSdkVersion 28
defaultConfig {
applicationId "io.nobullshit.nobullshit"
minSdkVersion 21
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "io.nobullshit.nobullshit.TIRunner"
}
buildTypes {
debug { }
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
sourceSets {
androidTest.java.srcDirs += "src/test-common/java"
test.java.srcDirs += "src/test-common/java"
}
}

// allows mocking for classes w/o directly opening them for release builds
allOpen {
annotation("io.nobullshit.nobullshit.testing.OpenForTesting")
}

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
// KOTLIN
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// ANDROID
implementation "androidx.appcompat:appcompat:$android_appcompat_version"
implementation "androidx.constraintlayout:constraintlayout:$android_constraint_layout_version"
implementation "com.google.android.material:material:$android_material_version"
implementation "androidx.paging:paging-runtime-ktx:$android_pagging_version"
// FIREBASE
implementation "com.google.firebase:firebase-core:$firebase_version"
implementation "com.firebaseui:firebase-ui-firestore:$firestore_ui_version"
// GLIDE
implementation "com.github.bumptech.glide:glide:$glide_version"
kapt "com.github.bumptech.glide:compiler:$glide_version"
// DAGGER 2
implementation "com.google.dagger:dagger:$dagger_version"
implementation "com.google.dagger:dagger-android:$dagger_version"
implementation "com.google.dagger:dagger-android-support:$dagger_version"
kapt "com.google.dagger:dagger-compiler:$dagger_version"
kapt "com.google.dagger:dagger-android-processor:$dagger_version"
compileOnly 'javax.annotation:jsr250-api:1.0'
// CUSTOM TABS
implementation "androidx.browser:browser:$custom_tab_version"
// UNIT TEST
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
testImplementation 'junit:junit:4.12'
// INSTRUMENTED TEST
androidTestImplementation 'androidx.test.ext:junit:1.1.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.1.1'
androidTestImplementation "io.mockk:mockk-android:1.8.13"
kaptAndroidTest 'com.google.dagger:dagger:2.17'
}
apply plugin: 'com.google.gms.google-services'
21 changes: 21 additions & 0 deletions android/app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.nobullshit.nobullshit

import android.app.Application
import android.content.Context
import androidx.test.runner.AndroidJUnitRunner
import io.nobullshit.nobullshit.base.TIBaseApplication

/**
* Custom runner to disable dependency injection.
*/
class TIRunner : AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader, className: String, context: Context): Application {
return super.newApplication(cl, TIBaseApplication::class.java.name, context)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.nobullshit.nobullshit.base

import android.app.Application

/**
* We use a separate App for tests to prevent initializing dependency injection.
*
* See [io.nobullshit.nobullshit.TIRunner].
*/
class TIBaseApplication : Application()
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package io.nobullshit.nobullshit.ui.joblist

import android.content.Intent
import androidx.browser.customtabs.CustomTabsIntent
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.Intents.intending
import androidx.test.espresso.intent.matcher.IntentMatchers.*
import androidx.test.espresso.intent.rule.IntentsTestRule
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.rule.ActivityTestRule
import com.google.firebase.firestore.*
import io.mockk.*
import io.nobullshit.nobullshit.Datasets
import io.nobullshit.nobullshit.Datasets.SINGLE_JOB
import io.nobullshit.nobullshit.R
import io.nobullshit.nobullshit.db.dao.JobDao
import io.nobullshit.nobullshit.extension.getCategoryTitle
import io.nobullshit.nobullshit.extension.getTypeTitle
import io.nobullshit.nobullshit.model.Company
import io.nobullshit.nobullshit.model.Job
import io.nobullshit.nobullshit.testing.SingleFragmentActivity
import io.nobullshit.nobullshit.util.*
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.not

import org.junit.Test
import org.junit.runner.RunWith

import org.junit.Rule

/**
* Instrumented tests for [JobListFragment]
*/
@RunWith(AndroidJUnit4::class)
@LargeTest
class TIJobList {

@Rule
@JvmField
val activityRule = IntentsTestRule(SingleFragmentActivity::class.java, true, true)

@Test
fun testLoadingSingleJob() {
this.configureFragmentBeforeTest(mockQuery(Job::class.java, Datasets.SINGLE_JOB))

onView(withId(R.id.fragment_job_list_refresh)).check(matches(not(isRefreshing())))
onView(withId(R.id.fragment_job_list_rv)).check(matches((hasItemCount(1))))
onView(allOf(withId(R.id.item_job_title), withText(Datasets.SINGLE_JOB.title))).check(matches(isDisplayed()))
onView(allOf(withId(R.id.item_job_company_title), withText(Datasets.SINGLE_JOB.company.title))).check(matches(isDisplayed()))
onView(withId(R.id.item_job_chip_group)).check(matches(hasChildCount(2)))
onView(withId(R.id.item_job_chip_group)).check(matches(atPositionChipGroup(0, withText(Datasets.SINGLE_JOB.getCategoryTitle(activityRule.activity)))))
onView(withId(R.id.item_job_chip_group)).check(matches(atPositionChipGroup(1, withText(Datasets.SINGLE_JOB.getTypeTitle(activityRule.activity)))))
}

@Test
fun testLoadingMultipleJob() {
this.configureFragmentBeforeTest(mockQuery(Job::class.java, *Datasets.MULTIPLE_JOBS))

onView(withId(R.id.fragment_job_list_refresh)).check(matches(not(isRefreshing())))
onView(withId(R.id.fragment_job_list_rv)).check(matches((hasItemCount(30))))
}

@Test
fun testLoadingNothing() {
this.configureFragmentBeforeTest(mockQuery(Job::class.java))

onView(withId(R.id.fragment_job_list_refresh)).check(matches(not(isRefreshing())))
onView(withId(R.id.fragment_job_list_rv)).check(matches((hasItemCount(0))))
}

@Test
fun testClickOnJob() {
this.configureFragmentBeforeTest(mockQuery(Job::class.java, Datasets.SINGLE_JOB))

onView(withId(R.id.fragment_job_list_rv)).perform(RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(0, click()))
intended(hasAction(Intent.ACTION_VIEW))
intended(hasData(Datasets.SINGLE_JOB.url))
}

// ---

/**
* Create a new [JobListFragment]
* and mock the [JobDao] with custom response [Query].
* The fragment will be set inside a fake activity [SingleFragmentActivity]
*/
private fun configureFragmentBeforeTest(query: Query) {
val fragment = JobListFragment().apply {
val firestore = mockk<FirebaseFirestore>()
jobDao = object : JobDao(firestore) {
override fun listApprovedJobs() = query
}
}
activityRule.activity.setFragment(fragment)
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package io.nobullshit.nobullshit.util

import com.google.android.gms.tasks.TaskExecutors
import com.google.android.gms.tasks.Tasks
import com.google.firebase.firestore.*
import io.mockk.every
import io.mockk.mockk
import java.util.concurrent.Callable
import java.util.concurrent.Executor

/**
* Mock a [FirebaseFirestore] [Query]
* with custom generic object(s) [models]
*/
fun <T> mockQuery(valueType: Class<T>, vararg models: T): Query {

// First query that getting mocked data
val mockQuery = mockk<Query>()
val mockSnapshot = mockk<QuerySnapshot>()
val mockDocuments = mutableListOf<QueryDocumentSnapshot>()

/**
* Create a mock of each [DocumentSnapshot]
* and fill it with an object [T] from [models]
*/
models.forEach {
val mockDocument = mockk<QueryDocumentSnapshot>(relaxed = true)
every { mockDocument.toObject(valueType) } returns it
every { mockQuery.startAfter(mockDocument) } returns mockQuery
mockDocuments.add(mockDocument)
}
every { mockSnapshot.documents } returns mockDocuments.toList()

// Second query that must return EMPTY data to cancel future loads
val mockQueryEmpty = mockk<Query>(relaxed = true)
val mockSnapshotEmpty = mockk<QuerySnapshot>()
every { mockSnapshotEmpty.documents } returns listOf<DocumentSnapshot>()

/**
* Mocking two requests [Tasks].
* The first one will get mocked data.
* The second will return empty list of [DocumentSnapshot].
* Both tasks use [TaskExecutors.MAIN_THREAD] executor.
*/
every { mockQuery.get(Source.DEFAULT) } returns Tasks.call { mockSnapshot }
every { mockQuery.limit(60) } returns mockQuery
every { mockQuery.limit(20) } returns mockQueryEmpty
every { mockQueryEmpty.get(Source.DEFAULT) } returns Tasks.call { mockSnapshotEmpty }

return mockQuery
}
Loading

0 comments on commit 7eac46b

Please sign in to comment.