mirror of
https://github.com/SAP/jenkins-library.git
synced 2026-06-19 22:58:55 +02:00
Initial check-in of lesfurets test improvements (#23)
Adding lesfurets test framwork improvements via helper classes
This commit is contained in:
@@ -46,6 +46,12 @@
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jenkins-ci.plugins</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>1.23</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jenkins-ci.plugins.workflow</groupId>
|
||||
<artifactId>workflow-aggregator</artifactId>
|
||||
@@ -84,7 +90,19 @@
|
||||
<version>1.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.codehaus.groovy</groupId>
|
||||
<artifactId>groovy-all</artifactId>
|
||||
<!-- any version of Groovy \>= 1.5.0 should work here -->
|
||||
<version>2.4.12</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.yaml/snakeyaml -->
|
||||
<dependency>
|
||||
<groupId>org.yaml</groupId>
|
||||
<artifactId>snakeyaml</artifactId>
|
||||
@@ -108,7 +126,6 @@
|
||||
</goals>
|
||||
<configuration>
|
||||
<sources>
|
||||
<source>test/java</source>
|
||||
<source>test/groovy</source>
|
||||
</sources>
|
||||
</configuration>
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.sap.piper
|
||||
|
||||
import com.lesfurets.jenkins.unit.BasePipelineTest
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.ExpectedException
|
||||
import org.junit.rules.RuleChain
|
||||
import util.JenkinsLoggingRule
|
||||
import util.JenkinsSetupRule
|
||||
import util.SharedLibraryCreator
|
||||
|
||||
import static org.hamcrest.Matchers.containsString
|
||||
import static org.hamcrest.Matchers.hasSize
|
||||
import static org.junit.Assert.assertEquals
|
||||
import static org.junit.Assert.assertThat
|
||||
|
||||
class UtilsTest extends BasePipelineTest {
|
||||
|
||||
@Rule
|
||||
public ExpectedException exception = ExpectedException.none()
|
||||
|
||||
@Rule
|
||||
public JenkinsSetupRule setUpRule = new JenkinsSetupRule(this, SharedLibraryCreator.lazyLoadedLibrary)
|
||||
|
||||
Utils utils
|
||||
|
||||
@Before
|
||||
void init() throws Exception {
|
||||
utils = new Utils()
|
||||
prepareObjectInterceptors(utils)
|
||||
}
|
||||
|
||||
void prepareObjectInterceptors(object) {
|
||||
object.metaClass.invokeMethod = helper.getMethodInterceptor()
|
||||
object.metaClass.static.invokeMethod = helper.getMethodInterceptor()
|
||||
object.metaClass.methodMissing = helper.getMethodMissingInterceptor()
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetMandatoryParameterValid() {
|
||||
|
||||
def sourceMap = [test1: 'value1', test2: 'value2']
|
||||
|
||||
def defaultFallbackMap = [myDefault1: 'default1']
|
||||
|
||||
assertEquals('value1', utils.getMandatoryParameter(sourceMap, 'test1', null))
|
||||
|
||||
assertEquals('value1', utils.getMandatoryParameter(sourceMap, 'test1', ''))
|
||||
|
||||
assertEquals('value1', utils.getMandatoryParameter(sourceMap, 'test1', 'customValue'))
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetMandatoryParameterDefaultFallback() {
|
||||
|
||||
def myMap = [test1: 'value1', test2: 'value2']
|
||||
|
||||
assertEquals('', utils.getMandatoryParameter(myMap, 'test3', ''))
|
||||
assertEquals('customValue', utils.getMandatoryParameter(myMap, 'test3', 'customValue'))
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testGetMandatoryParameterFail() {
|
||||
|
||||
def myMap = [test1: 'value1', test2: 'value2']
|
||||
|
||||
exception.expect(Exception.class)
|
||||
|
||||
exception.expectMessage("ERROR - NO VALUE AVAILABLE FOR")
|
||||
|
||||
utils.getMandatoryParameter(myMap, 'test3', null)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package util
|
||||
|
||||
import com.lesfurets.jenkins.unit.BasePipelineTest
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.Description
|
||||
import org.junit.runners.model.Statement
|
||||
|
||||
class JenkinsLoggingRule implements TestRule {
|
||||
|
||||
final BasePipelineTest testInstance
|
||||
|
||||
String log = ""
|
||||
|
||||
JenkinsLoggingRule(BasePipelineTest testInstance) {
|
||||
this.testInstance = testInstance
|
||||
}
|
||||
|
||||
@Override
|
||||
Statement apply(Statement base, Description description) {
|
||||
return statement(base)
|
||||
}
|
||||
|
||||
private Statement statement(final Statement base) {
|
||||
return new Statement() {
|
||||
@Override
|
||||
void evaluate() throws Throwable {
|
||||
|
||||
testInstance.helper.registerAllowedMethod("echo", [String.class], {
|
||||
echoInput ->
|
||||
log += "$echoInput \n"
|
||||
})
|
||||
|
||||
base.evaluate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package util
|
||||
|
||||
import com.lesfurets.jenkins.unit.BasePipelineTest
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.Description
|
||||
import org.junit.runners.model.Statement
|
||||
|
||||
class JenkinsPropertiesRule implements TestRule {
|
||||
|
||||
final BasePipelineTest testInstance
|
||||
|
||||
final String propertyPath
|
||||
|
||||
final Properties configProperties
|
||||
|
||||
JenkinsPropertiesRule(BasePipelineTest testInstance, String propertyPath) {
|
||||
this.testInstance = testInstance
|
||||
this.propertyPath = propertyPath
|
||||
configProperties = loadProperties(propertyPath)
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
Statement apply(Statement base, Description description) {
|
||||
return statement(base)
|
||||
}
|
||||
|
||||
private Statement statement(final Statement base) {
|
||||
return new Statement() {
|
||||
@Override
|
||||
void evaluate() throws Throwable {
|
||||
|
||||
testInstance.helper.registerAllowedMethod("readProperties", [Map.class], {
|
||||
propertyPath ->
|
||||
if (JenkinsPropertiesRule.this.propertyPath.contains(propertyPath.file)) {
|
||||
return JenkinsPropertiesRule.this.configProperties
|
||||
}
|
||||
|
||||
throw new Exception("Could not find the properties with path $propertyPath")
|
||||
})
|
||||
|
||||
base.evaluate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Properties loadProperties(String path) {
|
||||
def inputStream = new File(path).newInputStream()
|
||||
def properties = new Properties()
|
||||
properties.load(inputStream)
|
||||
inputStream.close()
|
||||
return properties
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package util
|
||||
|
||||
import com.lesfurets.jenkins.unit.BasePipelineTest
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.Description
|
||||
import org.junit.runners.model.Statement
|
||||
|
||||
class JenkinsScriptLoaderRule implements TestRule {
|
||||
|
||||
final BasePipelineTest testInstance
|
||||
|
||||
final String scriptBasePath
|
||||
|
||||
JenkinsScriptLoaderRule(BasePipelineTest testInstance, String scriptBasePath) {
|
||||
this.testInstance = testInstance
|
||||
this.scriptBasePath = scriptBasePath
|
||||
}
|
||||
|
||||
@Override
|
||||
Statement apply(Statement base, Description description) {
|
||||
return statement(base)
|
||||
}
|
||||
|
||||
private Statement statement(final Statement base) {
|
||||
return new Statement() {
|
||||
@Override
|
||||
void evaluate() throws Throwable {
|
||||
|
||||
testInstance.helper.registerAllowedMethod("load", [String.class], {
|
||||
fileNameIntegration ->
|
||||
return testInstance.loadScript("$scriptBasePath/$fileNameIntegration")
|
||||
})
|
||||
|
||||
base.evaluate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package util
|
||||
|
||||
import com.lesfurets.jenkins.unit.BasePipelineTest
|
||||
import com.lesfurets.jenkins.unit.global.lib.LibraryConfiguration
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.Description
|
||||
import org.junit.runners.model.Statement
|
||||
|
||||
class JenkinsSetupRule implements TestRule {
|
||||
|
||||
def library = SharedLibraryCreator.implicitLoadedLibrary
|
||||
|
||||
final BasePipelineTest testInstance
|
||||
|
||||
JenkinsSetupRule(BasePipelineTest testInstance) {
|
||||
this.testInstance = testInstance
|
||||
}
|
||||
|
||||
JenkinsSetupRule(BasePipelineTest testInstance, LibraryConfiguration configuration) {
|
||||
this.testInstance = testInstance
|
||||
this.library = configuration
|
||||
}
|
||||
|
||||
@Override
|
||||
Statement apply(Statement base, Description description) {
|
||||
return statement(base)
|
||||
}
|
||||
|
||||
private Statement statement(final Statement base) {
|
||||
return new Statement() {
|
||||
@Override
|
||||
void evaluate() throws Throwable {
|
||||
|
||||
testInstance.scriptRoots += "vars/"
|
||||
testInstance.setUp()
|
||||
// register library
|
||||
testInstance.helper.registerSharedLibrary(library)
|
||||
// set jenkins job mock variables
|
||||
testInstance.binding.setVariable('env', [
|
||||
JOB_NAME : 'p',
|
||||
BUILD_NUMBER: '1',
|
||||
BUILD_URL : ''
|
||||
])
|
||||
|
||||
base.evaluate()
|
||||
|
||||
testInstance.printCallStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
#!groovy
|
||||
package util
|
||||
|
||||
import groovy.json.JsonBuilder
|
||||
import groovy.json.JsonSlurper
|
||||
import hudson.tasks.junit.TestResult
|
||||
import org.yaml.snakeyaml.Yaml
|
||||
|
||||
/**
|
||||
* This is a Helper class for mocking.
|
||||
*
|
||||
* It can be used to load test data or to mock Jenkins or Maven specific objects.
|
||||
**/
|
||||
|
||||
class MockHelper {
|
||||
|
||||
/**
|
||||
* load properties from resources for mocking return value of readProperties method
|
||||
* @param path to properties
|
||||
* @return properties file
|
||||
*/
|
||||
Properties loadProperties( String path ){
|
||||
Properties p = new Properties()
|
||||
File pFile = new File( path )
|
||||
p.load( pFile.newDataInputStream() )
|
||||
return p
|
||||
}
|
||||
|
||||
/**
|
||||
* load JSON from resources for mocking return value of readJSON method
|
||||
* @param path to json file
|
||||
* @return json file
|
||||
*/
|
||||
Object loadJSON( String path ){
|
||||
def js = new JsonSlurper()
|
||||
def reader = new BufferedReader(new FileReader( path ))
|
||||
def j = js.parse(reader)
|
||||
return j
|
||||
}
|
||||
|
||||
/**
|
||||
* load YAML from resources for mocking return value of readYaml method
|
||||
* @param path to yaml file
|
||||
* @return yaml file
|
||||
*/
|
||||
Object loadYAML( String path ){
|
||||
return new Yaml().load(new FileReader(path))
|
||||
}
|
||||
|
||||
/**
|
||||
* creates HTTP response for mocking return value of httpRequest method
|
||||
* @param text - text to parse into json object
|
||||
* @return json Object
|
||||
*/
|
||||
Object createResponse( String text ){
|
||||
def response = new JsonBuilder(new JsonSlurper().parseText( text ))
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* load File from resources for mocking return value of readFile method
|
||||
* @param path to file
|
||||
* @return File
|
||||
*/
|
||||
File loadFile( String path ){
|
||||
return new File( path )
|
||||
}
|
||||
|
||||
/**
|
||||
* load POM from resources for mocking return value of readMavenPom method
|
||||
* @param path to pom file
|
||||
* @return Pom class
|
||||
*/
|
||||
MockPom loadPom(String path ){
|
||||
return new MockPom( path )
|
||||
}
|
||||
|
||||
/**
|
||||
* Inner class to mock maven descriptor
|
||||
*/
|
||||
class MockPom {
|
||||
def f
|
||||
def pom
|
||||
MockPom(String path){
|
||||
this.f = new File( path )
|
||||
if ( f.exists() ){
|
||||
this.pom = new XmlSlurper().parse(f)
|
||||
}
|
||||
else {
|
||||
throw new FileNotFoundException( 'Failed to find file: ' + path )
|
||||
}
|
||||
}
|
||||
String getVersion(){
|
||||
return pom.version
|
||||
}
|
||||
String getGroupId(){
|
||||
return pom.groupId
|
||||
}
|
||||
String getArtifactId(){
|
||||
return pom.artifactId
|
||||
}
|
||||
String getPackaging(){
|
||||
return pom.packaging
|
||||
}
|
||||
String getName(){
|
||||
return pom.name
|
||||
}
|
||||
}
|
||||
|
||||
MockBuild loadMockBuild(){
|
||||
return new MockBuild()
|
||||
}
|
||||
|
||||
MockBuild loadMockBuild(TestResult result){
|
||||
return new MockBuild(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* Inner class to mock Jenkins' currentBuild return object in scripts
|
||||
*/
|
||||
class MockBuild {
|
||||
TestResult testResult
|
||||
MockBuild(){}
|
||||
MockBuild(TestResult result){
|
||||
testResult = result
|
||||
}
|
||||
MockLibrary getAction(Class c){
|
||||
println("MockLibrary -> getAction - arg: " + c.toString() )
|
||||
return new MockLibrary()
|
||||
}
|
||||
|
||||
class MockLibrary {
|
||||
MockLibrary(){}
|
||||
// return default
|
||||
List getLibraries(){
|
||||
println("MockLibrary -> getLibraries")
|
||||
return [ [name: 'default-library', version: 'default-master', trusted: true] ]
|
||||
}
|
||||
TestResult getResult() {
|
||||
println("MockLibrary -> getResult")
|
||||
return testResult
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#!groovy
|
||||
package util
|
||||
|
||||
import com.lesfurets.jenkins.unit.global.lib.SourceRetriever
|
||||
|
||||
/**
|
||||
* Retrieves the shared lib sources of the current project which are expected to be
|
||||
* at the default location "./vars".
|
||||
*/
|
||||
|
||||
class ProjectSource implements SourceRetriever {
|
||||
|
||||
private def sourceDir = new File('.')
|
||||
|
||||
/*
|
||||
* None of the parameters provided in the signature are used in the use-case of that retriever.
|
||||
*/
|
||||
List<URL> retrieve(String repository, String branch, String targetPath) {
|
||||
if (sourceDir.exists()) {
|
||||
return [sourceDir.toURI().toURL()]
|
||||
}
|
||||
throw new IllegalStateException("Directory $sourceDir.path does not exists!")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package util
|
||||
|
||||
import static com.lesfurets.jenkins.unit.global.lib.LibraryConfiguration.library
|
||||
|
||||
class SharedLibraryCreator {
|
||||
|
||||
static def lazyLoadedLibrary = library()
|
||||
.name('piper-library')
|
||||
.retriever(new ProjectSource())
|
||||
.targetPath('is/not/necessary')
|
||||
.defaultVersion("master")
|
||||
.allowOverride(true)
|
||||
.implicit(false)
|
||||
.build()
|
||||
|
||||
static def implicitLoadedLibrary = library()
|
||||
.name('piper-library')
|
||||
.retriever(new ProjectSource())
|
||||
.targetPath('is/not/necessary')
|
||||
.defaultVersion("master")
|
||||
.allowOverride(true)
|
||||
.implicit(true)
|
||||
.build()
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import javax.inject.Inject;
|
||||
import jenkins.model.Jenkins;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.jenkinsci.plugins.workflow.cps.global.UserDefinedGlobalVariableList;
|
||||
import org.jenkinsci.plugins.workflow.cps.global.WorkflowLibRepository;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Rule;
|
||||
import org.jvnet.hudson.test.BuildWatcher;
|
||||
import org.jvnet.hudson.test.RestartableJenkinsRule;
|
||||
|
||||
public class AbstractJenkinsTest {
|
||||
@ClassRule
|
||||
public static BuildWatcher buildWatcher = new BuildWatcher();
|
||||
@Rule
|
||||
public RestartableJenkinsRule story = new RestartableJenkinsRule();
|
||||
@Inject
|
||||
protected Jenkins jenkins;
|
||||
@Inject
|
||||
WorkflowLibRepository repo;
|
||||
@Inject
|
||||
protected UserDefinedGlobalVariableList uvl;
|
||||
|
||||
public AbstractJenkinsTest() {
|
||||
super();
|
||||
}
|
||||
|
||||
protected void copyLibrarySources() {
|
||||
try {
|
||||
FileUtils.copyDirectory(new File("vars"), new File(repo.workspace, "vars"));
|
||||
FileUtils.copyDirectory(new File("src"), new File(repo.workspace, "src"));
|
||||
FileUtils.copyDirectory(new File("resources"), new File(repo.workspace, "resources"));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
|
||||
import org.jenkinsci.plugins.workflow.cps.global.UserDefinedGlobalVariable;
|
||||
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
|
||||
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
|
||||
import org.junit.Test;
|
||||
import org.junit.runners.model.Statement;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
|
||||
|
||||
public class AcmeTest extends AbstractJenkinsTest {
|
||||
|
||||
/**
|
||||
* Test acme getter and setter
|
||||
*/
|
||||
@Test
|
||||
public void acmeTest() throws Exception {
|
||||
story.addStep(new Statement() {
|
||||
@Override public void evaluate() throws Throwable {
|
||||
//File vars = new File(repo.workspace, UserDefinedGlobalVariable.PREFIX);
|
||||
File vars = new File(repo.workspace, "vars");
|
||||
vars.mkdirs();
|
||||
FileUtils.writeStringToFile(new File(vars, "acme.groovy"), StringUtils.join(Arrays.asList(
|
||||
"class acme implements Serializable {",
|
||||
" private String name = 'initial'",
|
||||
" def setName(value) {",
|
||||
" this.name = value",
|
||||
" }",
|
||||
" def getName() {",
|
||||
" this.name",
|
||||
" }",
|
||||
" def caution(message) {",
|
||||
" echo \"Hello, ${name}! CAUTION: ${message}\"",
|
||||
" }",
|
||||
"}")
|
||||
, "\n"));
|
||||
|
||||
// simulate the effect of push
|
||||
uvl.rebuild();
|
||||
|
||||
WorkflowJob p = jenkins.createProject(WorkflowJob.class, "p");
|
||||
|
||||
p.setDefinition(new CpsFlowDefinition(
|
||||
"node {\n" +
|
||||
|
||||
"acme.setName('acmeName')\n"+
|
||||
"echo acme.getName()\n" +
|
||||
|
||||
"}",
|
||||
true));
|
||||
|
||||
// build this workflow
|
||||
WorkflowRun b = story.j.assertBuildStatusSuccess(p.scheduleBuild2(0));
|
||||
|
||||
story.j.assertLogContains("acmeName", b);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//@Test
|
||||
public void acmeTest2() throws Exception {
|
||||
story.addStep(new Statement() {
|
||||
@Override
|
||||
public void evaluate() throws Throwable {
|
||||
|
||||
copyLibrarySources();
|
||||
// simulate the effect of push
|
||||
uvl.rebuild();
|
||||
|
||||
WorkflowJob p = jenkins.createProject(WorkflowJob.class, "p");
|
||||
|
||||
p.setDefinition(new CpsFlowDefinition(
|
||||
"import com.sap.piper.Utils\n" +
|
||||
"node {\n" +
|
||||
|
||||
"acme.setName('myName')\n"+
|
||||
"assert acme.getName() == 'myName'\n" +
|
||||
|
||||
|
||||
"}",
|
||||
true));
|
||||
|
||||
// build this workflow
|
||||
WorkflowRun b = story.j.assertBuildStatusSuccess(p.scheduleBuild2(0));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import java.io.File;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
|
||||
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
|
||||
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
|
||||
import org.junit.runners.model.Statement;
|
||||
|
||||
public class _TemplateTest extends AbstractJenkinsTest {
|
||||
/**
|
||||
* Test ... step
|
||||
*/
|
||||
//@Test
|
||||
public void testWhatEver() throws Exception {
|
||||
story.addStep(new Statement() {
|
||||
@Override
|
||||
public void evaluate() throws Throwable {
|
||||
copyLibrarySources();
|
||||
// simulate the effect of push
|
||||
uvl.rebuild();
|
||||
WorkflowJob p = jenkins.createProject(WorkflowJob.class, "p");
|
||||
//copy test resources into workspace
|
||||
FileUtils.copyDirectory(new File("test/resources"), new File(jenkins.getWorkspaceFor(p).getRemote(), "resources"));
|
||||
|
||||
p.setDefinition(new CpsFlowDefinition(
|
||||
"node {\n" +
|
||||
|
||||
"\n" +
|
||||
"\n" +
|
||||
|
||||
"}",
|
||||
true)
|
||||
);
|
||||
|
||||
// build this workflow
|
||||
WorkflowRun b = story.j.assertBuildStatusSuccess(p.scheduleBuild2(0));
|
||||
//story.j.assertLogContains("this is part of the log", b);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user