package peergos.server.tests;
import org.junit.*;
import org.junit.runner.*;
import org.junit.runners.*;
import peergos.server.storage.ResetableFileInputStream;
import peergos.shared.*;
import peergos.shared.crypto.*;
import peergos.shared.crypto.symmetric.*;
import peergos.server.*;
import peergos.shared.user.*;
import peergos.shared.user.fs.*;
import peergos.shared.util.*;
import java.io.*;
import java.lang.reflect.*;
import java.net.*;
import java.nio.file.*;
import java.util.*;
import java.util.stream.*;
import static org.junit.Assert.assertTrue;
@RunWith(Parameterized.class)
public class MultiUserTests {
private final NetworkAccess network;
private final Crypto crypto;
private final int userCount;
public MultiUserTests(String useIPFS, Random r, int userCount, Crypto crypto) throws Exception {
int portMin = 9000;
int portRange = 2000;
int webPort = portMin + r.nextInt(portRange);
int corePort = portMin + portRange + r.nextInt(portRange);
this.userCount = userCount;
if (userCount < 2)
throw new IllegalStateException();
Args args = Args.parse(new String[]{"useIPFS", ""+useIPFS.equals("IPFS"), "-port", Integer.toString(webPort), "-corenodePort", Integer.toString(corePort)});
Start.local(args);
this.network = NetworkAccess.buildJava(new URL("http://localhost:" + webPort)).get();
this.crypto = crypto;
// use insecure random otherwise tests take ages
setFinalStatic(TweetNaCl.class.getDeclaredField("prng"), new Random(1));
}
static void setFinalStatic(Field field, Object newValue) throws Exception {
field.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
field.set(null, newValue);
}
@Parameterized.Parameters(name = "{index}: {0}")
public static Collection<Object[]> parameters() {
Random r = new Random(123);
Crypto crypto = Crypto.initJava();
return Arrays.asList(new Object[][] {
{"RAM", r, 2, crypto}
});
}
private static String username(int i){
return "username_"+i;
}
private List<UserContext> getUserContexts(int size) {
return IntStream.range(0, size)
.mapToObj(e -> {
String username = username(e);
try {
return UserTests.ensureSignedUp(username, username, network.clear(), crypto);
} catch (Exception ioe) {
throw new IllegalStateException(ioe);
}}).collect(Collectors.toList());
}
@Test
public void shareAndUnshareFile() throws Exception {
UserContext u1 = UserTests.ensureSignedUp("a", "a", network.clear(), crypto);
// send follow requests from each other user to "a"
List<UserContext> userContexts = getUserContexts(userCount);
for (UserContext userContext : userContexts) {
userContext.sendFollowRequest(u1.username, SymmetricKey.random()).get();
}
// make "a" reciprocate all the follow requests
List<FollowRequest> u1Requests = u1.processFollowRequests().get();
for (FollowRequest u1Request : u1Requests) {
boolean accept = true;
boolean reciprocate = true;
u1.sendReplyFollowRequest(u1Request, accept, reciprocate).get();
}
// complete the friendship connection
for (UserContext userContext : userContexts) {
userContext.processFollowRequests().get();//needed for side effect
}
// upload a file to "a"'s space
FileTreeNode u1Root = u1.getUserRoot().get();
String filename = "somefile.txt";
File f = File.createTempFile("peergos", "");
byte[] originalFileContents = "Hello Peergos friend!".getBytes();
Files.write(f.toPath(), originalFileContents);
ResetableFileInputStream resetableFileInputStream = new ResetableFileInputStream(f);
boolean uploaded = u1Root.uploadFile(filename, resetableFileInputStream, f.length(),
u1.network, u1.crypto.random,l -> {}, u1.fragmenter()).get();
// share the file from "a" to each of the others
FileTreeNode u1File = u1.getByPath(u1.username + "/" + filename).get().get();
u1.shareWith(Paths.get(u1.username, filename), userContexts.stream().map(u -> u.username).collect(Collectors.toSet())).get();
// check other users can read the file
for (UserContext userContext : userContexts) {
Optional<FileTreeNode> sharedFile = userContext.getByPath(u1.username + "/" + UserContext.SHARED_DIR_NAME +
"/" + userContext.username + "/" + filename).get();
Assert.assertTrue("shared file present", sharedFile.isPresent());
AsyncReader inputStream = sharedFile.get().getInputStream(userContext.network,
userContext.crypto.random, l -> {}).get();
byte[] fileContents = Serialize.readFully(inputStream, sharedFile.get().getFileProperties().size).get();
Assert.assertTrue("shared file contents correct", Arrays.equals(originalFileContents, fileContents));
}
UserContext userToUnshareWith = userContexts.stream().findFirst().get();
// unshare with a single user
u1.unShare(Paths.get(u1.username, filename), userToUnshareWith.username).get();
List<UserContext> updatedUserContexts = getUserContexts(userCount);
//test that the other user cannot access it from scratch
Optional<FileTreeNode> otherUserView = updatedUserContexts.get(0).getByPath(u1.username + "/" + filename).get();
Assert.assertTrue(! otherUserView.isPresent());
List<UserContext> remainingUsers = updatedUserContexts.stream()
.skip(1)
.collect(Collectors.toList());
UserContext u1New = UserTests.ensureSignedUp("a", "a", network.clear(), crypto);
// check remaining users can still read it
for (UserContext userContext : remainingUsers) {
String path = u1.username + "/" + UserContext.SHARED_DIR_NAME +
"/" + userContext.username + "/" + filename;
Optional<FileTreeNode> sharedFile = userContext.getByPath(path).get();
Assert.assertTrue("path '"+ path +"' is still available", sharedFile.isPresent());
}
// test that u1 can still access the original file
Optional<FileTreeNode> fileWithNewBaseKey = u1New.getByPath(u1.username + "/" + filename).get();
Assert.assertTrue(fileWithNewBaseKey.isPresent());
// Now modify the file
byte[] suffix = "Some new data at the end".getBytes();
AsyncReader suffixStream = new AsyncReader.ArrayBacked(suffix);
FileTreeNode parent = u1New.getByPath(u1New.username).get().get();
parent.uploadFileSection(filename, suffixStream, originalFileContents.length, originalFileContents.length + suffix.length,
Optional.empty(), u1New.network, u1New.crypto.random, l -> {}, u1New.fragmenter());
AsyncReader extendedContents = u1New.getByPath(u1.username + "/" + filename).get().get().getInputStream(u1New.network,
u1New.crypto.random, l -> {}).get();
byte[] newFileContents = Serialize.readFully(extendedContents, originalFileContents.length + suffix.length).get();
Assert.assertTrue(Arrays.equals(newFileContents, ArrayOps.concat(originalFileContents, suffix)));
}
private String random() {
return UUID.randomUUID().toString();
}
public void shareAndUnshareFolder(int userCount) throws Exception {
Assert.assertTrue(0 < userCount);
String u1nameAndPasword = "a";
UserContext u1 = UserTests.ensureSignedUp(u1nameAndPasword, u1nameAndPasword, network, crypto);
// UserContext u2 = UserTests.ensureSignedUp("b", "b", webPort);
List<UserContext> users = new ArrayList<>();
List<String> userNames = new ArrayList<>(), userPasswords = new ArrayList<>();
for (int i = 0; i < userCount; i++) {
userNames.add(random());
userPasswords.add(random());
}
for (int i = 0; i < userCount; i++)
users.add(UserTests.ensureSignedUp(userNames.get(i), userPasswords.get(i), network, crypto));
for (UserContext user : users)
user.sendFollowRequest(u1.username, SymmetricKey.random()).get();
List<FollowRequest> u1Requests = u1.processFollowRequests().get();
for (FollowRequest u1Request : u1Requests) {
boolean accept = true;
boolean reciprocate = true;
u1.sendReplyFollowRequest(u1Request, accept, reciprocate).get();
}
for (UserContext user : users) {
user.processFollowRequests().get();
}
// friends are connected
// share a file from u1 to u2
FileTreeNode u1Root = u1.getUserRoot().get();
String filename = "somefile.txt";
File f = File.createTempFile("peergos", "");
byte[] originalFileContents = "Hello Peergos friend!".getBytes();
Files.write(f.toPath(), originalFileContents);
String folderName = "afolder";
u1Root.mkdir(folderName, u1.network, SymmetricKey.random(), false, u1.crypto.random).get();
FileTreeNode folder = u1.getByPath("/a/" + folderName).get().get();
ResetableFileInputStream resetableFileInputStream = new ResetableFileInputStream(f);
boolean uploaded = folder.uploadFile(filename, resetableFileInputStream, f.length(), u1.network,
u1.crypto.random, l -> {}, u1.fragmenter()).get();
String originalPath = u1.username + "/" + folderName + "/" + filename;
FileTreeNode file = u1.getByPath(originalPath).get().get();
byte[] fileContents = null;
for (UserContext user : users) {
String path = u1.username + "/" + UserContext.SHARED_DIR_NAME + "/" + user.username;
FileTreeNode u1ToU2 = u1.getByPath(path).get().get();
FileTreeNode fileTreeNode = u1ToU2.addLinkTo(folder, u1.network, u1.crypto.random).get();
FileTreeNode ownerViewOfLink = u1.getByPath(u1.username + "/" + UserContext.SHARED_DIR_NAME + "/" + user.username + "/" + folderName).get().get();
Set<FileTreeNode> u2children = user
.getByPath(path)
.get().get()
.getChildren(user.network).get();
Optional<FileTreeNode> fromParent = u2children.stream()
.filter(fn -> fn.getFileProperties().name.equals(folderName))
.findAny();
Assert.assertTrue("shared file present via parent's children", fromParent.isPresent());
Optional<FileTreeNode> sharedFolder = user.getByPath(u1.username + "/" + UserContext.SHARED_DIR_NAME + "/" + user.username + "/" + folderName).get();
Assert.assertTrue("Shared folder present via direct path", sharedFolder.isPresent() && sharedFolder.get().getFileProperties().name.equals(folderName));
FileTreeNode sharedFile = user.getByPath(u1.username + "/" + UserContext.SHARED_DIR_NAME + "/" + user.username + "/" + folderName + "/" + filename).get().get();
AsyncReader inputStream = sharedFile.getInputStream(user.network, user.crypto.random, l -> {}).get();
byte[] contents = Serialize.readFully(inputStream, sharedFile.getSize()).get();
if (fileContents != null)
Assert.assertTrue(
Arrays.equals(contents, fileContents)); //users share same view of data
fileContents = contents;
Assert.assertTrue("shared file contents correct", Arrays.equals(originalFileContents, fileContents));
}
// unshare
// for (UserContext user : users) {
// u1.unShare(Paths.get(u1nameAndPasword, folderName), user.username);
// }
//test that u2 cannot access it from scratch
UserContext u1New = UserTests.ensureSignedUp(u1nameAndPasword, u1nameAndPasword, network.clear(), crypto);
List<UserContext> usersNew = new ArrayList<>();
for (int i = 0; i < userCount; i++)
usersNew.add(UserTests.ensureSignedUp(userNames.get(i), userPasswords.get(i), network.clear(), crypto));
for (int i = 0; i < usersNew.size(); i++) {
UserContext user = usersNew.get(i);
u1.unShare(Paths.get(u1nameAndPasword, folderName), user.username);
Optional<FileTreeNode> updatedSharedFolder = user.getByPath(u1New.username + "/" + UserContext.SHARED_DIR_NAME + "/" + user.username + "/" + folderName).get();
// test that u1 can still access the original file
Optional<FileTreeNode> fileWithNewBaseKey = u1New.getByPath(u1New.username + "/" + folderName + "/" + filename).get();
Assert.assertTrue(!updatedSharedFolder.isPresent());
Assert.assertTrue(fileWithNewBaseKey.isPresent());
// Now modify the file
byte[] suffix = "Some new data at the end".getBytes();
AsyncReader suffixStream = new AsyncReader.ArrayBacked(suffix);
FileTreeNode parent = u1New.getByPath(u1New.username + "/" + folderName).get().get();
parent.uploadFileSection(filename, suffixStream, fileContents.length, fileContents.length + suffix.length,
Optional.empty(), u1New.network, u1New.crypto.random, l -> {}, u1New.fragmenter()).get();
FileTreeNode extendedFile = u1New.getByPath(originalPath).get().get();
AsyncReader extendedContents = extendedFile.getInputStream(u1New.network, u1New.crypto.random, l -> {}).get();
byte[] newFileContents = Serialize.readFully(extendedContents, extendedFile.getSize()).get();
Assert.assertTrue(Arrays.equals(newFileContents, ArrayOps.concat(fileContents, suffix)));
//test remaining users can still see shared file and folder
for (int j = i+1; j < usersNew.size(); j++) {
UserContext otherUser = usersNew.get(j);
Optional<FileTreeNode> sharedFolder = otherUser.getByPath(u1.username + "/" + UserContext.SHARED_DIR_NAME + "/" + otherUser.username + "/" + folderName).get();
Assert.assertTrue("Shared folder present via direct path", sharedFolder.isPresent() && sharedFolder.get().getFileProperties().name.equals(folderName));
FileTreeNode sharedFile = otherUser.getByPath(u1.username + "/" + UserContext.SHARED_DIR_NAME + "/" + otherUser.username + "/" + folderName + "/" + filename).get().get();
AsyncReader inputStream = sharedFile.getInputStream(otherUser.network, otherUser.crypto.random, l -> {}).get();
byte[] contents = Serialize.readFully(inputStream, sharedFile.getSize()).get();
Assert.assertTrue(Arrays.equals(contents, newFileContents)); //remaining users share latest view of same data
}
}
}
@Test
public void shareAndUnshareFolder() throws Exception {
shareAndUnshareFolder(4);
}
@Test
public void acceptAndReciprocateFollowRequest() throws Exception {
UserContext u1 = UserTests.ensureSignedUp("q", "q", network, crypto);
UserContext u2 = UserTests.ensureSignedUp("w", "w", network, crypto);
u2.sendFollowRequest(u1.username, SymmetricKey.random()).get();
List<FollowRequest> u1Requests = u1.processFollowRequests().get();
assertTrue("Receive a follow request", u1Requests.size() > 0);
u1.sendReplyFollowRequest(u1Requests.get(0), true, true).get();
List<FollowRequest> u2FollowRequests = u2.processFollowRequests().get();
Optional<FileTreeNode> u1ToU2 = u2.getByPath("/" + u1.username).get();
assertTrue("Friend root present after accepted follow request", u1ToU2.isPresent());
Optional<FileTreeNode> u2ToU1 = u1.getByPath("/" + u2.username).get();
assertTrue("Friend root present after accepted follow request", u2ToU1.isPresent());
Set<String> u1Following = UserTests.ensureSignedUp("q", "q", network.clear(), crypto).getSocialState().get()
.followingRoots.stream().map(f -> f.getName())
.collect(Collectors.toSet());
assertTrue("Following correct", u1Following.contains(u2.username));
Set<String> u2Following = UserTests.ensureSignedUp("w", "w", network.clear(), crypto).getSocialState().get()
.followingRoots.stream().map(f -> f.getName())
.collect(Collectors.toSet());
assertTrue("Following correct", u2Following.contains(u1.username));
}
@Test
public void acceptButNotReciprocateFollowRequest() throws Exception {
UserContext u1 = UserTests.ensureSignedUp("q", "q", network, crypto);
UserContext u2 = UserTests.ensureSignedUp("w", "w", network, crypto);
u2.sendFollowRequest(u1.username, SymmetricKey.random()).get();
List<FollowRequest> u1Requests = u1.processFollowRequests().get();
assertTrue("Receive a follow request", u1Requests.size() > 0);
u1.sendReplyFollowRequest(u1Requests.get(0), true, false).get();
List<FollowRequest> u2FollowRequests = u2.processFollowRequests().get();
Optional<FileTreeNode> u1Tou2 = u2.getByPath("/" + u1.username).get();
Optional<FileTreeNode> u2Tou1 = u1.getByPath("/" + u2.username).get();
assertTrue("Friend root present after accepted follow request", u1Tou2.isPresent());
assertTrue("Friend root not present after non reciprocated follow request", !u2Tou1.isPresent());
}
@Test
public void rejectFollowRequest() throws Exception {
UserContext u1 = UserTests.ensureSignedUp("q", "q", network, crypto);
UserContext u2 = UserTests.ensureSignedUp("w", "w", network, crypto);
u2.sendFollowRequest(u1.username, SymmetricKey.random());
List<FollowRequest> u1Requests = u1.processFollowRequests().get();
assertTrue("Receive a follow request", u1Requests.size() > 0);
u1.sendReplyFollowRequest(u1Requests.get(0), false, false);
List<FollowRequest> u2FollowRequests = u2.processFollowRequests().get();
Optional<FileTreeNode> u1Tou2 = u2.getByPath("/" + u1.username).get();
assertTrue("Friend root not present after rejected follow request", ! u1Tou2.isPresent());
Optional<FileTreeNode> u2Tou1 = u1.getByPath("/" + u2.username).get();
assertTrue("Friend root not present after non reciprocated follow request", !u2Tou1.isPresent());
}
@Test
public void reciprocateButNotAcceptFollowRequest() throws Exception {
UserContext u1 = UserTests.ensureSignedUp("q", "q", network, crypto);
UserContext u2 = UserTests.ensureSignedUp("w", "w", network, crypto);
u2.sendFollowRequest(u1.username, SymmetricKey.random());
List<FollowRequest> u1Requests = u1.processFollowRequests().get();
assertTrue("Receive a follow request", u1Requests.size() > 0);
u1.sendReplyFollowRequest(u1Requests.get(0), false, true);
List<FollowRequest> u2FollowRequests = u2.processFollowRequests().get();
Optional<FileTreeNode> u1Tou2 = u2.getByPath("/" + u1.username).get();
assertTrue("Friend root not present after rejected follow request", ! u1Tou2.isPresent());
Optional<FileTreeNode> u2Tou1 = u1.getByPath("/" + u2.username).get();
assertTrue("Friend root present after reciprocated follow request", u2Tou1.isPresent());
}
@Test
public void unfollow() throws Exception {
UserContext u1 = UserTests.ensureSignedUp("q", "q", network, crypto);
UserContext u2 = UserTests.ensureSignedUp("w", "w", network, crypto);
u2.sendFollowRequest(u1.username, SymmetricKey.random());
List<FollowRequest> u1Requests = u1.processFollowRequests().get();
u1.sendReplyFollowRequest(u1Requests.get(0), true, true);
List<FollowRequest> u2FollowRequests = u2.processFollowRequests().get();
Set<String> u1Following = u1.getFollowing().get();
Assert.assertTrue("u1 following u2", u1Following.contains(u2.username));
u1.unfollow(u2.username).get();
Set<String> newU1Following = u1.getFollowing().get();
Assert.assertTrue("u1 no longer following u2", ! newU1Following.contains(u2.username));
}
@Test
public void removeFollower() throws Exception {
UserContext u1 = UserTests.ensureSignedUp("q", "q", network, crypto);
UserContext u2 = UserTests.ensureSignedUp("w", "w", network, crypto);
u2.sendFollowRequest(u1.username, SymmetricKey.random());
List<FollowRequest> u1Requests = u1.processFollowRequests().get();
u1.sendReplyFollowRequest(u1Requests.get(0), true, true);
List<FollowRequest> u2FollowRequests = u2.processFollowRequests().get();
Set<String> u1Followers = u1.getFollowerNames().get();
Assert.assertTrue("u1 following u2", u1Followers.contains(u2.username));
u1.removeFollower(u2.username).get();
Set<String> newU1Followers = u1.getFollowerNames().get();
Assert.assertTrue("u1 no longer has u2 as follower", ! newU1Followers.contains(u2.username));
}
}