Skip to content

Commit f7687c2

Browse files
authored
Merge pull request #68 from DevinciHQ/dev-282-frontend-test-anon
Dev 282 frontend test anon
2 parents e27192f + 6228c14 commit f7687c2

File tree

9 files changed

+220
-79
lines changed

9 files changed

+220
-79
lines changed

backend/handlers/api/query.py

+18-9
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,24 @@ def query_handler():
1919

2020
tags = get_tags(search_string)
2121
query_string = get_query(search_string)
22+
2223
# Return 400 bad request error if the search_string contains only
2324
# hashtags and no actual search query.
2425
if len(query_string) == 0:
2526
abort(400)
2627
user = None
28+
29+
# If we get an unauthenticated query request, just go ahead and redirect them to google for now
30+
# without recording it in the database. (We may change this behavior soon)
31+
if not security.is_request_with_auth(request):
32+
return jsonify(ApiResponse({'redirect': create_google_redirect(query_string)}))
33+
34+
# The request WAS trying to authenticate, so let's try to get the authenticated user.
2735
try:
2836
user = security.authenticate_user(request)
2937
except security.ValidationError as err:
30-
escaped_q = urllib.urlencode({'q': query_string})
31-
redirect = 'http://google.com/#' + escaped_q
32-
return jsonify(ApiResponse({'redirect': redirect}))
38+
logging.debug(err)
39+
abort(401)
3340

3441
# Get the user-agent header from the request.
3542
user_agent = parseUA(request.headers['User-Agent'])
@@ -58,12 +65,7 @@ def query_handler():
5865
# Save to the datatore.
5966
query.put()
6067
logging.debug('query: %s', str(query))
61-
62-
escaped_q = urllib.urlencode({'q': query_string})
63-
redirect = 'http://google.com/#' + escaped_q
64-
65-
# response.headers['Content-Type'] = 'application/json'
66-
return jsonify(ApiResponse({'redirect': redirect}))
68+
return jsonify(ApiResponse({'redirect': create_google_redirect(query_string)}))
6769

6870

6971
def get_tags(search_string):
@@ -98,3 +100,10 @@ def get_query(search_string):
98100
search_query.append(i)
99101
cursor = False
100102
return " ".join(search_query)
103+
104+
105+
def create_google_redirect(search_string):
106+
"""convert a string into a google url that will show search results."""
107+
escaped_q = urllib.urlencode({'q': search_string})
108+
redirect = 'https://google.com/#' + escaped_q
109+
return redirect

backend/shared/security.py

+6
Original file line numberDiff line numberDiff line change
@@ -245,5 +245,11 @@ def get_referrer_insecure(req):
245245
logging.warn("Got a malformed referrer.")
246246

247247

248+
def is_request_with_auth(req):
249+
"""Check if the request has an auth header."""
250+
auth_header = req.headers.get('Authorization', False)
251+
if not auth_header:
252+
return False
253+
return True
248254
# Create the global pubkey object so that other code can use it.
249255
PUBKEY = PublicKey()

backend/tests/handlers_api_query_test.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def test_anon_query(self):
6060
# Suppressing the pylint error for no-member
6161
# pylint: disable=maybe-no-member
6262
data = json.loads(rv.data) # type: Response
63-
self.assertEqual(data['payload']['redirect'], "http://google.com/#q=test")
63+
self.assertEqual(data['payload']['redirect'], "https://google.com/#q=test")
6464

6565
def test_empty_hashtag(self):
6666
""" Testing the empty hashtag case."""
@@ -91,6 +91,10 @@ def test_remove_hashtags(self):
9191
search_query = query.get_query("#this is a #test")
9292
self.assertEqual(search_query, "is a #test")
9393

94+
def test_create_google_redirect(self):
95+
redirect = query.create_google_redirect("&? Search test")
96+
self.assertEqual(redirect, "https://google.com/#q=%26%3F+Search+test")
97+
9498
def open_with_auth(self, url, method):
9599
fake_token = get_fake_jwt_token()
96100
return self.app.open(url, method=method, headers={"Authorization": "Bearer " + fake_token},

backend/tests/security_test.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -147,5 +147,12 @@ def test_authentication(self):
147147
security.set_auth_session_cookie(auth_request)
148148
user = security.authenticate_user(empty_request)
149149

150+
def test_is_request_with_auth(self):
151+
with app.test_request_context():
152+
# Expect that requests without an Authorization header return False
153+
empty_request = MockRequest(headers={})
154+
self.assertEqual(security.is_request_with_auth(empty_request), False)
150155

151-
156+
# Expect that requests with an Authorization header return True;
157+
auth_request = MockRequest(headers={"Authorization": "anything"})
158+
self.assertEqual(security.is_request_with_auth(auth_request), True)

default/src/app/query/query.service.spec.ts

+88-5
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import {
66
} from '@angular/core/testing';
77
import { QueryService } from './query.service';
88
import { BackendService} from '../shared/backend.service';
9-
import { Observable, Observer } from 'rxjs';
9+
import { Observable, Observer, BehaviorSubject } from 'rxjs';
10+
import { User } from '../shared/user';
11+
import { AuthService } from '../auth/auth.service';
1012

1113

1214
class MockBackendService {
1315

14-
request(path: string, data: any) {
16+
request(path: string, data: any, use_auth: boolean) {
1517
return Observable.create(
1618
(observer: Observer<any>) => {
1719
observer.next(null);
@@ -21,25 +23,47 @@ class MockBackendService {
2123
}
2224
}
2325

26+
class MockAuthService {
27+
28+
private user = new BehaviorSubject<User>(null);
29+
// Observable string streams
30+
// user$ = this.user.asObservable();
31+
32+
login() {
33+
this.user.next(new User(new User({uid: '123abc', email: 'fake.test@example.com', loggedIn: true})));
34+
}
35+
36+
logout() {
37+
this.user.next(new User);
38+
}
39+
40+
getUser() {
41+
return this.user;
42+
}
43+
44+
}
45+
2446
describe('QueryService', () => {
2547
beforeEach(() => {
2648
addProviders([
2749
QueryService,
50+
{provide: AuthService, useClass: MockAuthService},
2851
{provide: BackendService, useClass: MockBackendService}
2952
]);
3053
});
3154

32-
it('should call backend path with query and source as data.', async(inject(
55+
it('should call backend path with query, source and authentication as data.', async(inject(
3356
// Note that we pass XHRBackend, NOT MockBackend.
34-
[QueryService, BackendService], (queryService: QueryService, backend: MockBackendService) => {
57+
[QueryService, AuthService, BackendService], (queryService: QueryService, auth: MockAuthService, backend: MockBackendService) => {
3558

3659
let query = 'asdf';
3760
let source = 'test';
3861

3962
spyOn(backend, 'request').and.callThrough();
4063

64+
auth.login();
4165
queryService.doQuery('asdf', 'test').subscribe(data => {
42-
expect(backend.request).toHaveBeenCalledWith('/api/q', {q: query, source: source});
66+
expect(backend.request).toHaveBeenCalledWith('/api/q', {q: query, source: source}, true);
4367
},
4468
err => {
4569
throw new Error('Query failed');
@@ -48,6 +72,65 @@ describe('QueryService', () => {
4872

4973
}
5074
)));
75+
it('should use an un-authenticated request if the user is logged out.', async(inject(
76+
// Note that we pass XHRBackend, NOT MockBackend.
77+
[QueryService, AuthService, BackendService], (queryService: QueryService, auth: MockAuthService, backend: MockBackendService) => {
78+
79+
let query = 'asdf';
80+
let source = 'test';
81+
82+
spyOn(backend, 'request').and.callThrough();
83+
auth.logout();
84+
85+
queryService.doQuery('asdf', 'test').subscribe(data => {
86+
expect(backend.request).toHaveBeenCalledWith('/api/q', {q: query, source: source}, false);
87+
},
88+
err => {
89+
throw new Error('Query failed');
90+
}
91+
);
92+
93+
}
94+
)));
95+
it('should use an authenticated request if the user is logged in.', async(inject(
96+
// Note that we pass XHRBackend, NOT MockBackend.
97+
[QueryService, AuthService, BackendService], (queryService: QueryService, auth: MockAuthService, backend: MockBackendService) => {
98+
99+
let query = 'asdf';
100+
let source = 'test';
101+
102+
spyOn(backend, 'request').and.callThrough();
103+
auth.login();
104+
105+
queryService.doQuery('asdf', 'test').subscribe(data => {
106+
expect(backend.request).toHaveBeenCalledWith('/api/q', {q: query, source: source}, true);
107+
},
108+
err => {
109+
throw new Error('Query failed');
110+
}
111+
);
112+
113+
}
114+
)));
115+
it('should not make any request if the user is null.', async(inject(
116+
// Note that we pass XHRBackend, NOT MockBackend.
117+
[QueryService, AuthService, BackendService], (queryService: QueryService, auth: MockAuthService, backend: MockBackendService) => {
118+
119+
spyOn(backend, 'request').and.callThrough();
120+
121+
// We don't login or logout, so the user should be null.
122+
123+
queryService.doQuery('asdf', 'test').subscribe(data => {
124+
expect(backend.request).not.toHaveBeenCalled();
125+
},
126+
err => {
127+
throw new Error('Query failed');
128+
}
129+
);
130+
131+
}
132+
)));
133+
51134
describe('getQueries()', () => {
52135

53136
it('should call backend.request() just the /api/report path and an empty object.', async(inject(

default/src/app/query/query.service.ts

+18-2
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,35 @@ import { Injectable } from '@angular/core';
33
import { Observable } from 'rxjs/Observable';
44
import 'rxjs/add/operator/catch';
55
import {BackendService} from '../shared/backend.service';
6+
import {AuthService} from '../auth/auth.service';
67

78
@Injectable()
89
export class QueryService {
9-
constructor(private backend: BackendService) {}
10+
constructor(private backend: BackendService, private auth: AuthService) {}
1011

1112
/**
1213
*
1314
* @param query
15+
* @param source
1416
* @returns {Observable<Object>}
1517
*/
1618
public doQuery(query: string, source: string): Observable<Object> {
19+
let user = this.auth.getUser().getValue();
20+
let use_auth: boolean;
21+
22+
// Send an authenticated request if the user is logged in.
23+
if (user && user.loggedIn) {
24+
use_auth = true;
25+
} else if (user && !user.loggedIn) {
26+
// Send an un-authenticated request if the user is logged out.
27+
use_auth = false;
28+
} else {
29+
// If the user is still pending (null), then don't do anything yet.
30+
console.log('Not making any request until the user\'s loggedIn status is resolved.');
31+
return Observable.empty();
32+
}
1733
// Save the search query and get the JSON response (which also contains link to redirect) in return.
18-
return this.backend.request('/api/q', {q: query, source: source });
34+
return this.backend.request('/api/q', {q: query, source: source }, use_auth);
1935
}
2036

2137
// Get the search history as a JSON response.

default/src/app/search/search.component.spec.ts

+29-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {addProviders, inject } from '@angular/core/testing';
22
import { SearchComponent } from './index';
33
import { QueryService } from '../query/index';
44
import { AuthService} from '../auth/auth.service';
5-
import { Observable, BehaviorSubject } from 'rxjs';
5+
import { Observable, BehaviorSubject, Observer } from 'rxjs';
66
import { User } from '../shared/user';
77

88
class MockQueryService {
@@ -78,4 +78,32 @@ describe('SearchComponent', () => {
7878
expect(value).toBe('some search stuff');
7979
})
8080
);
81+
82+
it('anonymous should be able to search and then get redirected.',
83+
inject([SearchComponent, QueryService, AuthService], (component: SearchComponent, querySrv: MockQueryService,
84+
auth: MockAuthService) => {
85+
spyOn(querySrv, 'doQuery').and.callFake(() => {
86+
return Observable.create(
87+
(observer: Observer<any>) => {
88+
observer.next({
89+
'success': 'true',
90+
'payload': {
91+
'redirect': 'http://google.com/q#=whatever',
92+
},
93+
'cursor': null
94+
});
95+
observer.complete();
96+
}
97+
);
98+
});
99+
100+
// We can get around private method issues like this.
101+
let redirect = spyOn(component, '_redirect');
102+
auth.logout();
103+
component.submit('whatever');
104+
expect(querySrv.doQuery).toHaveBeenCalled();
105+
expect(redirect).toHaveBeenCalledWith('http://google.com/q#=whatever');
106+
})
107+
108+
);
81109
});

default/src/app/search/search.component.ts

+1-14
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,9 @@ import {AuthService} from '../auth/auth.service';
1212
export class SearchComponent {
1313

1414
private preSearchText: any;
15-
private disabled = true;
1615
constructor(private queryService: QueryService, private auth: AuthService) {
1716
this.preSearchText = this.populateSearch(window.location.href);
1817
this.recordOmniSearch(window.location.href);
19-
this.auth.getUser().subscribe(
20-
user => {
21-
if (user && user.loggedIn) {
22-
this.disabled = false;
23-
} else {
24-
this.disabled = true;
25-
}
26-
},
27-
err => {
28-
console.log('authEvent', err);
29-
}
30-
);
3118
}
3219

3320
submit(searchField: string) {
@@ -46,7 +33,7 @@ export class SearchComponent {
4633
this.queryService.doQuery(searchField, 'site-search').subscribe(
4734
data => {
4835
// If when data is returned from a query with a redirect set, do the redirect.
49-
if (data['payload']['redirect']) {
36+
if (data['payload'] && data['payload']['redirect']) {
5037
this._redirect(data['payload']['redirect']);
5138
}
5239
}

0 commit comments

Comments
 (0)